理解 Window 和 WindowManager

全面了解 Window

Posted by Song on April 3, 2021

Window 和 WindowManager

  • Window表示的是一个窗口的概念,创建一个Window很简单,只需要WindowManager去实现
  • WindowManager是外界访问Window的入口,Window的具体实现是在WindowManagerService中,他们两个的交互是一个IPC的过程
  • Android中的所有视图都是通过Window来实现的,无论是Activity,Dialog还是Toast,他们的视图都是直接附加在Window上的,因此Window是View的直接管理者,在之前的事件分发中我们说到,View的事件是通过WIndow传递给DecorView,然后DecorView传递给我们的View,就连Activity的setContentView,都是由Window传递的
1
2
3
4
5
6
7
8
9
10
11
12
Button btn = new Button(this);
btn.setText("我是窗口");
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
WindowManager.LayoutParams layout = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSLUCENT);
layout.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
layout.gravity = Gravity.CENTER;
layout.type = WindowManager.LayoutParams.TYPE_PHONE;
layout.x = 300;
layout.y = 100;
wm.addView(btn, layout);
  • FLAG_NOT_FOCUSABLE 表示窗口不需要获取焦点,也不需要接收各种事件,这属性会同时启动FLAG_NOT_TOUCH_MODAL,最终的事件会传递给下层的具体焦点的window

  • FLAG_NOT_TOUCH_MODAL, 在此模式下,系统会将当前window区域以外的单击事件传递给底层的Window,此前的Window区域以内的单机事件自己处理,这个标记很重要,一般来说都需要开启,否则其他window将无法获取单击事件

  • FLAG_SHOW_WHEN_LOCKED 开启这个属性可以让window显示在锁屏上

    Type参数表示window的类型,window有三种类型,分别是应用,子,系统,应用window对应一个Activity,子Window不能单独存在,需要依赖一个父Window,比如常见的Dialog都是子Window,系统window需要声明权限,比如系统的状态栏

  • Window是分层的,每个Window对应着z-ordered,层级大的会覆盖在层级小的Window上面
  • 在这三类中,应用是层级范围是1-99,子window的层级是1000-1999,系统的层级是2000-2999。这些范围对应着type参数,如果想要window在最顶层,那么层级范围设置大一点就好了,很显然系统的值要大一些,系统的值很多,我们一般会选择TYPE_SYSTEM_OVERLAY和TYPE_SYSTEM_ERROR,但是需要设置权限。
1
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

Window 的内部机制

Window是一个抽象的概念,每一个Window对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl建立关系,因此Window并不是实际存在的,这点从WindowManager定义的接口都是针对View,这说明View才是window的实体,在实际使用当中我们并不能直接访问。

Window 的添加过程

Window 的添加过程是通过 WindowManager 的 addView 去实现的,而真正实现的是一个接口 WindowManagerImpl,而最终将所有的操作全部委托给 WindowManagerGlobal 去实现。

创建ViewRootImpl并将View添加到列表中

在WindowManagerGlobal有如下几个列表是比较重要的

1
2
3
4
5
    private final ArrayList<View> mViews = new ArrayList<View>();
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
    private final ArraySet<View> mDyingViews = new ArraySet<View>();

在上面的声明中,mViews存储所有window所对应的View,mRoots存储是所有window所对应的ViewRootImpl,mParams存储是所对应的布局参数,而mDyingViews则存储那些正在被删除的对象,在addView中通过如下方式将Window的一系列对象添加到列表中

1
2
3
4
5
6
7
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

通过ViewRootImpl来更新界面并完成Window的添加

这个步骤由ViewRootImpl的setView完成,他内部会通过 requstLayout 来完成异步刷新请求,在下面代码中,scheduleTraversals实际上就是View绘制的入口

1
2
3
4
5
6
7
8
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

接下来会通过WindowSession最终来完成Window的添加过程,在下面的代码中,WindowSession的类型IWindowSession,他是一个Binder对象,也是一次真正意义上的IPC操作

如此一来,Window的添加请求就交给WindowManagerService去处理了,在WindowManagerService内部为每一个应用添加了一个单独的Session,具体WIndow在WindowManagerService中怎么添加的,我们就不去分析了,读者可以自己去熟悉下

Window 的删除过程

Window的删除过程和添加过程一样,都是通过Impl再通过Global来实现。removeViewLocked是通过ViewRootImpl来完成删除操作的,在windowmanager中提供了两种接口removeView和removeViewImmediate,他们分别表示异步删除和同步删除,其中removeViewImmediate,使用起来要格外注意,一般来说不需要使用此方法来删除window以免发生意外的错误,这里主要是异步删除的问题,具体的删除操作是ViewImple的die方法来完成的,在异步删除的情况下,die只是发生一个删除的请求后就返回了,这个时候View并没有完成删除的操作,所有最后会将其添加到mDyingViews中,mDyingViews表示待删除的View列表,ViewRootImpe的die方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   boolean die(boolean immediate) {
        // Make sure we do execute immediately if we are in the middle of a traversal or the damage
        // done by dispatchDetachedFromWindow will cause havoc on return.
        if (immediate && !mIsInTraversal) {
            doDie();
            return false;
        }

        if (!mIsDrawing) {
            destroyHardwareRenderer();
        } else {
            Log.e(TAG, "Attempting to destroy the window while drawing!\n" +
                    "  window=" + this + ", title=" + mWindowAttributes.getTitle());
        }
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }

在die方法内部只是做了简单的判断,那么就发送了一个MSG_DIE的消息,ViewRootImpl中的mHandler会处理此消息并且并调用doDie方法,如果是同步删除就会直接调用doDie方法,在doDie方法内部会操作dispatchDetachedFromWindow,真正删除window就是在这里面实现的,他主要做了四件事

  1. 垃圾回收相关的工作,比如清除数据和消息,移除回调
  2. 通过Session的remove方法来删除window,这同样是一个IPC的过程,最终会调用wms的removeWindow方法
  3. 调用view的dispatchDetachedFromWindow 方法,在内不会调用onDetachedFromWindow,他做了一些回收资源或者停止动画的一些操作
  4. 调用WindowManagerGlobal的doRemoveView方法刷新数据

Window 的更新过程

updateViewLayout做的方法比较简单,首先他更新View的LayoutParams替换老的,接着再更新下ViewRootimpl中的LayoutParams,这一步是通过viewrootimpl的setLayoutParams来做的,在ViewRootImpl中会通过scheduleTraversals方法来对View,测量,布局,重绘等等,除了view本身的重绘之外,ViewRootImpl还会通过WindowSession来更新Window的视图,这个过程最终是WindowManagerService的relayoutWindow来实现的,具体也是一个IPC的过程

Window 的创建过程

Activity的Window创建过程

如果没有DecorView就去创建他

DecorView是一个FrameLayout,在之前就已经说过了,这里说一下。DecorView是activity中的顶级View,一般来说他的内部包含标题栏和内部栏,但是这个会随着主题的变化而发生改变的,不管怎么样,内容是一定要存在的,并且内容有固定的id,那就是content,完整的就是android.R.id.content,DecorView的创建是由installDecor方法来完成的,在方法内部会通过generateDecor方法来完成创建DecorView,这个时候他就是一个空白的FrameLayout

1
2
3
protected DecorView generateDecor(){
    return new DecorView(getContext(),-1);
}

为了初始化DecorView的结构,PhoneWindow还需要通过generateLayout方法来加载具体的布局文件到DecorView中,这个跟主题有关:

1
2
3
4
View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

其中ID_ANDROID_CONTENT的定义如下,这个id对应的就是ViewGroup的mContentParent

1
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

将View添加到DecorView的mContentParent中

这个过程比较简单,由于在第一步的时候已经初始化了DecorView,因此这一部就直接将activity的视图添加到DecorView的mContentParent中既可,mLayoutInflater.inflate(layoutResID, mContentParent);到此为止,由此可以理解activity的setcontentview的来历了,也许有读者会怀疑,为什么不叫setview来,他明明是给activity设置视图啊,从这里来看,他的确不适合叫做setview,因为activity的布局文件只是添加到了DecorView的mContentParent中,因此交setContentView更加准确。

回调Activity的onCreateChanged方法来通知Activity视图已经发生改变

这个过程很简单,由于window实现了Callback接口,这里表示布局已经被添加到DecorView的mContentParent中了,于是通知activity。使其可以做相应的处理Activity的onCreateChanged是一个空实现,我们可以在子activity处理这个回调

1
2
3
4
        final callback cb = getCallback();
        if(cb != null && !isDestroyed()){
            cb.onContentChanged();
        }

经过了上面的三个步骤,到这里为止DecorView已经被创建并且初始化完毕了activity的布局文件也已经添加到了DecorView的内容中,但是这个时候DecorView还没有被windowmanager添加到window中,这里需要正确的理解window的概念,window更多的是表示一种抽象的功能集合,虽然说早在activity的attch中window就已经被创建了,但是这个时候由于DecorView还没有被windowmanager识别,所有还不能提供具体的功能,因为他还无法接收外界的输入,在activityThread的makeVisible中,才能被视图看到:

1
2
3
4
5
6
7
8
    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

Dialog的Window创建过程

  • 创建Window(PhoneWindow)
  • 初始化DecorView并将Dialog的师徒添加到DecorView
  • 将DecorView添加到window并且显示

应用token一般只有activity持有,故需要activity的context才能创建 Dialog,另外系统Window比较特殊,他可以不需要token

Toast的Window创建过程

Toast和dialog不同,他的工作过稍微复杂一点,首先Toast也是基于Window来实现的,但是由于Toast具有定时取消的功能,所以系统采用了handler,在toast内部有两类IPC的过程,第一类Toast访问NotificationManagerService,第二类是NotificationManagerService回调toast的TN接口