关于WindowManager在Android N和Android N以下表现差异的分析总结

Posted this.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于WindowManager在Android N和Android N以下表现差异的分析总结相关的知识,希望对你有一定的参考价值。

1. 问题描述

通过WindowManager往窗口里添加浮动按钮,在android7.0时该按钮可以全局保留,直至进程被杀掉。而Android7.0以下(以Android4.4为例)浮动按钮随Activity的onStop()方法被覆盖。
以下为浮动按钮的实现代码:

WindowManager mWm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.type= LayoutParams.TYPE_PHONE;     // 系统提示类型,重要
params.format=1;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 
params.flags = params.flags | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
params.flags = params.flags | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 
params.width = dipToPixel(60);
params.height = dipToPixel(60);

params.x = dipToPixel(140);
params.y = dipToPixel(230);

View view = LayoutInflater.from(mContext).inflate(R.layout.float_view, null);
mWm.addView(view, params);
FloatView floatView = new FloatView(this); //传入Activity Context

2.解决方案

考虑到可能由于传入的Context为Activity导致的问题,进行修改后发现浮动按钮在Android4.4下正常运行。
FloatView floatView = new FloatView(getApplicationContext());
以下则以两方面分析差异原因:
* 使用Activity表现不同的原因。
* 使用Application表现相同的原因。

3.原因分析

##### * 使用Activity表现不同的原因。
首先,可以了解到Activity对getSystemService方法进行了重写:

@Override
    public Object getSystemService(@ServiceName @NonNull String name) 
        if (getBaseContext() == null) 
            throw new IllegalStateException(
                    "System services not available to Activities before onCreate()");
        

        if (WINDOW_SERVICE.equals(name)) 
            return mWindowManager;
         else if (SEARCH_SERVICE.equals(name)) 
            ensureSearchManager();
            return mSearchManager;
        
        return super.getSystemService(name);
    

WindowManager在Activity初始化的时候就已经被创建。mWindowManager变量在ActivityThread的handleLaunchedActivityactivity.attach()方法中赋值。最终该变量指向了WindowManagerImpl对象。

public WindowManagerImpl createLocalWindowManager(Window parentWindow) 
        return new WindowManagerImpl(mContext, parentWindow);
    

此处的parentWindow为当前Activity的Window
之后使用WindowManageraddView方法添加按钮,最终会调用WindowManagerGlobaladdView方法。其中该方法有如下语句:

 if (parentWindow != null) 
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        

从该该方法开始,表现出现差异:

//Android 7.0
 void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) 
        CharSequence curTitle = wp.getTitle();
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) 
            if (wp.token == null) 
                View decor = peekDecorView();
                if (decor != null) 
                    wp.token = decor.getWindowToken();
                
            
            //...
         else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) 
            if (curTitle == null || curTitle.length() == 0) 
                final StringBuilder title = new StringBuilder(32);
                title.append("Sys").append(wp.type);
                if (mAppName != null) 
                    title.append(":").append(mAppName);
                
                wp.setTitle(title);
            
         else 
            if (wp.token == null) 
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            
            if ((curTitle == null || curTitle.length() == 0)
                    && mAppName != null) 
                wp.setTitle(mAppName);
            
        
       //...
    
//Android 4.4
 void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) 
        CharSequence curTitle = wp.getTitle();
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) 
            if (wp.token == null) 
                View decor = peekDecorView();
                if (decor != null) 
                    wp.token = decor.getWindowToken();
                
            
            //...
         else 
            if (wp.token == null) 
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            
            if ((curTitle == null || curTitle.length() == 0)
                    && mAppName != null) 
                wp.setTitle(mAppName);
            
        
       //...
    

代码大概的逻辑是判断该LayoutParamstype是属于应用窗口级别,还是系统窗口级别。从而为LayoutParams添加对应的token
此处token可以用来绑定ActivityWindowManager。当视图发生变更需要重新遍历或者调用onStop覆盖掉当前Activity时,就可以通过该token找到对应的ViewRootImpl,从而进行View树的相关操作。
在Android7.0时,系统窗口没有设置token,4.4时,token设置为默认的mAppToken,即Activity启动时由AMS创建并传递过来的IBinder对象。
在Activity进入后台后,会调用Activity中的performStop方法,方法中执行以下语句:
WindowManagerGlobal.getInstance().setStoppedState(mToken, true);
从而调用了setStoppedState方法:

public void setStoppedState(IBinder token, boolean stopped) 
        synchronized (mLock) 
            int count = mViews.size();
            for (int i = 0; i < count; i++) 
                //token是Activity的mToken 不为空
                if (token == null || mParams.get(i).token == token) 
                    ViewRootImpl root = mRoots.get(i);
                    root.setWindowStopped(stopped);
                
            
        
    

当LayoutParams的token与Activity的token匹配时,则取出ViewRootImpl调用setWindowStopped方法,从而执行onStop过程的后续操作。
Android7.0 token为null,不匹配,无法进入判断,则不进行对浮动按钮的后续操作,所以会常驻留窗口。
Android4.4 token为Activity持有的mToken,匹配,会执行后续操作。
##### * 使用Application表现相同的原因。
Application没有对getSystemService进行重写,全部实现交由ContextImpl实现。

@Override
    public Object getSystemService(String name) 
        return SystemServiceRegistry.getSystemService(this, name);
    

上述从SystemServiceRegistry取出相应服务。SystemServiceRegistry在APP创建时会初始化各种服务。其中包括WindowManager

registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() 
            @Override
            public WindowManager createService(ContextImpl ctx) 
                return new WindowManagerImpl(ctx);
);

通过Application的getSystemService获得的WindowManager 仍然是WindowManagerImpl对象。Application和Activity创建的区别也在此处。

//Application调用
public WindowManagerImpl(Context context) 
        this(context, null);
    

//Activity调用
private WindowManagerImpl(Context context, Window parentWindow) 
        mContext = context;
        mParentWindow = parentWindow;

Application获得的WindowManagerImplmParentWindow属性为空。这样会在addView时跳过token的赋值。

 if (parentWindow != null) 
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
 

从而在setStopState方法判断mParams.get(i).token == token中不论是7.0还是4.4中token都为null,不匹配,无法进入后续的stop操作,浮动按钮就常驻窗口。

4. 个人理解

Android系统为每个Activity初始化时添加一个本地的WindowManager引用。由于Activity涉及频繁的View操作,对用WindowManager的使用频率较高,提前初始化对于效率会是一种提升。

以上是关于关于WindowManager在Android N和Android N以下表现差异的分析总结的主要内容,如果未能解决你的问题,请参考以下文章

关于ProgressDialog.show抛出android.view.WindowManager$BadTokenException: Unable to add window

[Android FrameWork 6.0源码学习] View的重绘过程之WindowManager的addView方法

Android WindowManager悬浮窗:不需要申请权限实现悬浮

Android.view.WindowManager $ BadTokenException:

android.view.WindowManager$BadTokenException

使用 WindowManager.addView 添加动态视图