浅析Android的窗口
Bugly 技术干货系列内容主要涉及移动开发方向,是由 Bugly 邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处。
一、窗口的概念
在开发过程中,我们经常会遇到,各种跟窗口相关的类,或者方法。但是,在 Android 的框架设计中,到底什么是窗口?窗口跟 Android Framework 中的 Window 类又是什么关系?以手Q 的主界面为例,如下图所示,上面的状态栏是一个窗口,手Q 的主界面自然是一个窗口,而弹出的 PopupWindow 也是一个窗口,我们经常使用的 Toast 也是一个窗口。像 Dialog,ContextMenu,以及 OptionMenu 等等这些都是窗口。这些窗口跟 Window 类的关系是什么,或者窗口跟 Window 类描述的是同一个概念吗?

其实窗口的概念,从不同的角度来看,其含义是不一样的。我们知道,WindowManagerService(后面简称 WmS)管理所有的窗口。但是对于WmS来讲,一个窗口其实就是一个 View 类,而不是 Window 类。WmS 负责管理这些 View 的 Z-order,显示区域,以及把消息派发到对应的 View 中。View 本身并不能直接从 WmS 中接收消息,而是通过实现了 IWindow 接口的 ViewRootImpl.W 类来实现,以下是这些类的关系:

所以这里窗口分为两层概念:
(1)WmS 眼中的,窗口是可以显示用来显示的 View。对于 WmS 而言,所谓的窗口就是一个通过 WindowManagerGlobal.addView()添加的 View 罢了;
(2)Window 类是一个针对窗口交互的抽象,也就是对于 WmS 来讲所有的用户消息是直接交给 View/ViewGroup 来处理的。而 Window 类把一些交互从 View/ViewGroup 中抽离出来,定义了一些窗口的行为,例如菜单,以及处理系统按钮,如“Home”,“Back”等等。由此可见,Window 描述的窗口只是在通用窗口的基础上,再抽象了一层,把符合某种规范的窗口统一了一下。Window 所描述的窗口,应该是通用窗口的一个子集。例如 PopupWindow 是一个窗口,但是分析其源码可以知道,该类并没有创建任何 Window 对象。而 Dialog 则是通过 PolicyManager.makeNewWindow(mContext) 创建了一个 Window 对象来管理窗口。当一个 Dialog 显示时,我们可以通过按 back 把它 dismiss 了,但是 PopupWindow 则不行,需要自己去处理。
二、窗口类型
添加一个窗口是通过 WindowManagerGlobal.addView()来完成的,分析 addView 方法的参数,有三个参数是必不可少的,view,params,以及 display。而 display 一般直接取 WindowMnagerImpl 中的 mDisplay,表示要输出的显示设备。view 自然表示要显示的 View,而 params 是 WindowManager.LayoutParams,用来描述这个 view 的些窗口属性,其中一个重要的参数 type,用来描述窗口的类型。
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
.....
}
分析 WindowManager 对于 type 可赋值类型的描述可知,Framework 中定义了三种类型的窗口:
(1)应用窗口 Activity 对应的窗口就是应用窗口, 所有 Activity 默认的窗口类型是 TYPE _BASE _APPLICATION。WindowManager 的 LayoutParams 的默认构建方法的实现,可以看到默认类型是 TYPE _ APPLICATION。 Dialog 的窗口类型是 TYPE _ APPLICATION,而很多 Dialog 的子类,修改了窗口类似,如 ContextMenu,本质是用 Dialog 来实现的,但是在添加窗口前,修改了 type 类型,赋值为 TYPE _ APPLICATION _ ATTACHED _ DIALOG。从这个我们可以看到,WmS 并没有把应用窗口与子窗口区分得那么清楚。
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}

(2)子窗口子窗口是指该窗口必须要有一个父窗口,父窗口可以是一个应用类型窗口,也可以是任何其他类型的窗口。例如前面手Q 界面中,点击右上角的按钮显示一个 PopupWindow,它就是一个子窗口,其类型一般 TYPE _ APPLICATION _ PANEL。既然称为子窗口,其与父窗口的关系是比较容易理解的。B 是 A 的子窗口,当 A 不可见时,B 也会不可见的。如果A不可见时添加B,B 也是不可见的,直到 A 可见为止,B 跟随一起可见。

(3)系统窗口 系统窗口跟应用窗口不同,不需要对应 Activity。跟子窗口不同,不需要有父窗口。一般来讲,系统窗口应该由系统来创建的,例如发生异常,ANR时的提示框,又如系统状态栏,屏保等。但是,Framework 还是定义了一些,可以被应用所创建的系统窗口,如 TYPE_ TOAST,TYPE _INPUT _ METHOD,TYPE _WALLPAPTER 等等。

token 的含义
相信大家对于 token 这个词并不陌生,在开发过程中经常遇到,例如 Bad Token 的异常。到底在 Android 框架中,token 代表什么?分析源码,我们发现,大多数 token 的对象,都表示一个 IBinder 对象。提到 IBinder,大家一点也不陌生,就是 Android 的 IPC 通信机制。在创建窗口过程中,涉及到的 IPC 通信,无非包含两方面,一个是 WmS 用来跟应用所在的进程进行通信的 ViewRootImpl.W 类的对象,另一个是指向一个 ActivityRecord 的对象,自然应该是WmS用来跟 AmS 进行通信的了。我们梳理了一下,token 以下几处的定义,分别来讲讲这里的 token 代表什么。

分析一下 View 的 AttachInfo 的赋值。ViewRootImpl 在构建方法里,会初始化一个 AttachInfo 实例,把它的 Session,以及 W类对象赋值给 AttachInfo。分析可以看到,AttachInfo 中的 mWindowToken,与mWindow 都是指向 ViewRootImpl 中的 mWindow(W类实例)。当一个 View attach 到窗口后,ViewRootImpl会执行performTraversals,如果发现是首次调用会,会把自己的 mAttachInfo 传递给根 View(通过dispatchAttachedToWindow),告诉 View 树现在已经 attch to Window 了,马上可以显示了。根 View(一般是 ViewGroup)会把这个信息,遍历地传递给 View 树中的每一个子 View,这样每个 View 的 mAttachInfo 都被赋值为 ViewRootImp 的 mAttachInfo了。
//分析一下 View 中的 AttachInfo 的赋值,以及 ViewRootImpl 中的 mAttachInfo
public ViewRootImpl(Context context, Display display) {
...
mWindow = new W(this);
...
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
...
}
//看到mWindowToken其实就是IWindow实例
AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {
mSession = session;
mWindow = window;
mWindowToken = window.asBinder();
mDisplay = display;
mViewRootImpl = viewRootImpl;
mHandler = handler;
mRootCallbacks = effectPlayer;
}
// ViewRootImpl在第一次执行performTraversals时,会把自己的mAttachInfo传递给根View,然后由根View逐级传递下去
private void performTraversals() {
...
if (mFirst) {
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
}else{
...
}
}
//ViewGroup.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
super.dispatchAttachedToWindow(info, visibility);
mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
