浅析 Android 的窗口

一、窗口的概念

在开发过程中,我们经常会遇到,各种跟窗口相关的类,或者方法。但是,在 Android 的框架设计中,到底什么是窗口?窗口跟 Android Framework 中的 Window 类又是什么关系?以手机QQ 的主界面为例,如下图所示,上面的状态栏是一个窗口,手机QQ 的主界面自然是一个窗口,而弹出的 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 _APPLICATIONWindowManager 的 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了。

WindowManager.LayoutParams 中的 type 与 token

WindowManager.LayoutParams 用来描述一个窗口的特性,最终在添加窗口时,会传递给 WmS。而且 WmS 会保存在 WindowState 的 mAttrs 中。LayoutParams 有很多参数,但是跟窗口创建相关的参数,最重要的就是 type 与 token 了,这里我们可以通过分析 WmS 的 addWindow 代码的可以知道:

总结一下:

(1)窗口类型必须是指定合法范围内的,即应用窗口,子窗口,系统窗口中的一种,否则检查会失败; (2)如果是系统,需要进行权限检查

以下类型不需要特别声明权限:

TYPE_TOAST, TYPE_DREAM, TYPE_INPUT_METHOD, TYPE_WALLPAPER, TYPE_PRIVATE_PRESENTATION, TYPE_VOICE_INTERACTION, TYPE_ ACCESSIBILITY_OVERLAY

以下类型需要声明使用权限:

android.permission.SYSTEM_ALERT_WINDOW_TYPE_PHONE, TYPE_PRIORITY_PHONE, TYPE_SYSTEM_ALERT, TYPE_SYSTEM_ERROR, TYPE_SYSTEM_OVERLAY

其他的系统窗口,需要声明权限:android.permission.INTERNAL_ SYSTEM WINDOW (3)如果是应用窗口,通过 token 检索出来的 WindowToken,一定不能为空,而且还必须是 Activity 的 mAppToken,同时对应的 Activity 还必须是没有被 finish。之前分析 Activity 的启动过程我们知道,Activity 在启动过程中,会先通过 WmS 的 addAppToken( )添加一个 AppWindowToken 到 mTokenMap 中,其中 key 就用了 IApplicationToken token。而 Activity 中的 mToken,以及 Activity 对应的 PhoneWindow 中的 mAppToken 就是来自 AmS 的 token (代码见 Activity 的 attach 方法)。 (4)如果是子窗口,会通过 attrs.token 去通过 windowForClientLocked 查找其父窗口,如果找不到其父窗口,会抛出异常。或者如果找到的父窗口的类型还是子窗口类型,也会抛出异常。这里查找父窗口的过程,是直接取了 attrs.token 去 mWindowMap 中找对应的 WindowState,而 mWindowMap 中的 key 是 IWindow。所以,由此可见,创建一个子窗口类型,token 必须赋值为其父窗口的 ViewRootImpl 中的 W 类对象 mWindow。 (5)如果是如下系统窗口,TYPE_INPUT_ METHOD,TYPE_VOICE_INTERACTION,TYPE_WALLPAPER,TYPE_DREAM,TYPE_ACCESSIBILITY_OVERLAY,token 不能为空,而且通过 token 检索到的 WindowToken 的类型不能是其本身对应的类型。 (6)如果是其他系统窗口,会直接把 attrs 中的 token 给清除了,不需要 token。因此其他类型的系统窗口,LayoutParams 中 token 是可以为空的。 (7)检查通过后,如果需要创建新的 WindowToken,会以 attrs.token 为 key,add 到 mTokenMap 中。 (8)WindowState 创建后,会以 IWindow 为 key (对应应用进程中的 ViewRootImpl.W 类对象 mWindow,重要的事强调多遍!!,添加到 mWindowMap 中。

由此可见,我们要成功添加一个窗口,对于 type 与 token 的赋值是有要求的,否则先不说能否正确显示,直接就创建失败了。那 type 与 token 是如何赋值的呢?最直接来讲,就是在调用 WindowManagerImpl 的 addView 方法前,把值赋好就可以了。但是,分析 FrameWork 所提供的一些窗口的显示,如 Dialog 等,并没有看到在调用 addView 之前,对 token 赋值呢。其窗口类型是应用窗口,根据前面所描述的检查,token 肯定不能为 null 的,而且还必须是 Activity 的 mAppToken,否则创建失败的。这里我们分析一下,在 token 没有赋值的情况下,调用 addView 会做哪些处理。代码就要回到 WindowManagerImpl 开始了。

这里的父窗口并不一定是真正意义上的父窗口,有可能就是描述一个窗口的 PhoneWindow 对象本身。有可能 PhoneWindow a,需要添加 a 窗口时,这里 parentWindow 有可能就是 a 对象本身。这里 WindowManagerImpl 中的 mParentWindow,到底代表什么,跟 WindowManagerImpl 的创建有关。例如 Activity 添加窗口的分析可知:

这里再总结一下,添加窗口时,WindowManager.Layout 的 token 的一些赋值情况: (1)无论是应用窗口,还是子窗口,以及部分系统窗口,token 是一定要赋值的。否则创建窗口会抛异常。对于应用窗口,token 必须是某个 Activity 的 mToken。对于子窗口而言,token 必须是其父窗口的 IWindow 对象。对于部分系统窗口而言,token 检索到的 WindowToken 的类型,不能再是其本身的类型了。 (2)可以在调用 WindowManagerImpl 的 addView 创建窗口前,直接把 token 赋值好。 (3)如果调用 WindowManagerImpl 的 addView 前,没有把 token 赋值好。这里会走一些默认逻辑,以保证 token 的合法性: A: 检查有没有设置默认 token,如果有,而且父窗口为 null,直接取设置的默认 token 吧。目前还没有遇到用这种方法设置 token 的情况。 B:如果有设置父窗口,则会调用父窗口的 adjustLayoutParamsForSubWindow 来检查 token。对于子窗口类型,会把父窗口的 IWindow 对象赋值给 token。对于应用窗口,或者系统窗口,直接取父窗口的 mAppToken。(父窗口是一个 Window 对象,其实例是 PhoneWindow。)

三、窗口的创建与移除

在分析窗口的创建与移除之前,我们先简单来介绍一下 Android 的 GUI 系统,它包含以下部分内容: (1)窗口和图形系统—Window and View Manager System (2)显示合成系统 — Surface Flinger (3)用户输出系统 — InputManager System (4)应用框架系统 — Activity Manager System 它们之间的关系,如下图所示:

简单来讲,Activity Manager System(重点是 ActivityManagerService,简称 AmS)负责 Activity 的启动,以及生命周期的管理。一个 Activity 对应一个应用窗口,这个窗口的创建以及管理是 Window and View Manager System 的职责。例如前面的截图,手机屏幕上显示了三个窗口,状态栏窗口,手Q 主界面窗口,以及一个 PopupWindow。View 系统管理每个窗口中复杂的布局,最终这个 View Hierarchy 最顶端的根 View 会被作为窗口,添加到 Window Manager System 中。Window Manager System 管理着所有这些添加的窗口,负责管理这些窗口的层次,显示位置等内容。每个窗口都有一块自己的Surface,Surface Flinger 负责把这些 Surface 合成一块 FrameBuffer。

也就是说 Window Manager System 负责窗口的创建与移除,以及显示状态的管理。具体绘制是由 Suerface Flinger 来负责的。

3.1 应用窗口的创建

首先,我们来分析应用窗口的创建,这也是我们开发过程中,最先遇到的。从开发第一个 Hello World 的 Android 应用开始,我们就已经在接触应用窗口了。

我们知道每个 Activity 对应一个 PhoneWindow,当我们调用 setContentView 时,其实最终结果是把我们的 View 树作为子 View 添加到 PhoneWindow 的 DecorView 中。也就是每个 Activity 的根 View 其实是一个 DecorView。而最终这个 DecorView,又是在 ActivityThread 的 handleResumeActivity 方法中,通过 WindowMnagerImpl 的 addView 方法添加到 WmS 中去的。 PhoneWindow 的 setContentView 又做了哪些事情呢?只是通过 installDecor()方法,给窗口初始化一了些装饰。而所谓的装饰就是指界面上看到的标题栏,导航栏 ActionBar。而我们通过 Activity 的 setContentView 设置的 View,是作为窗口的内容(如下图所示,是作为ID为android.R.content 的 FrameLayout 的子 View)。这里,我们是不是就理解了,窗口的标题栏是如何被添加的。以及窗口的一些属性为什么要在 setContentView 调用之前被设置了。因为,generateLayout 方法负责根据窗口的属性,最终决定采用不同的布局来生成窗口的顶层布局。而 generateLayout 只在 mContentParent 为 null 的时候被调用,而 mContentParent 只有在第一次调用 setContentView 时为 null,此后就不再为 null 了。

从上面的图可以知道,PhoneWindow 只是负责处理一些应用窗口通用的逻辑。但是真正完成把一个 View,作为窗口添加到 WmS 的过程是由 WindowManager 来完成的。WindowManager 在应用程序端的实现是 WindowManagerImpl,也就是说,我们通过 Activity 的 getWindowManager 获取到的实际上是 WindowManagerImpl。因此在在 ActivityThread 的 handleResumeActivity 方法中,有调用 WindowManagerImpl 的 addView,把 PhoneWindow 准备好的 DecorView 作为窗口添加到 WmS 中。代码如下所示:

窗口的添加过程如上图所示,我们知道 WmS 运行在单独的进程中。这里 IWindowSession 执行的 addtoDisplay 操作应该是 IPC 调用。每个应用窗口创建时,都会创建一个 ViewRootImpl 对象。分析了后面子窗口的创建,以及系统窗口的创建后,我们会知道其实任何一个窗口的创建,最终都是会创建一个 ViewRootImpl对象。ViewRootImpl 是一很重要的类,类似 ActivityThread 负责跟AmS通信一样,ViewRootImpl 的一个重要职责就是跟 WmS 通信,它通静态变量 sWindowSession(IWindowSession 实例)与 WmS 进行通信。每个应用进程,仅有一个 sWindowSession 对象,它对应了 WmS 中的 Session 子类,WmS 为每一个应用进程分配一个 Session 对象。WindowState 类有一个 IWindow mClient 参数,是在构造方法中赋值的,是由 Session 调用 addWindow 传递过来了,对应了 ViewRootImpl 中的 W 类的实例。

这里梳理了一下,这些类的对应关系。由此可以看到,创建一个窗口的本质过程,就是在 WmS 端创建一个用来表示这个窗口的 WindowState 对象。 WmS 负责管理这里些 WindowState 对象。

到此为止,一个应用窗口的创建过程就结束了。

总结一下,对于Acivity对应的应用窗口创建: (1)其 type 类型是TYPE_ BASE_ APPLICATION,定义为应用窗口; (2)用来执行 addView 操作的 WindowManager,到底是哪个呢?分析前面的源码,是通过 Activity 的 getWindowManager 来获取的,而返回的是 Activity 中的 mWindowManager,而它的赋值又是在 attch 方法中,直接取的 Window 的 mWindowManager。Window 的 mWindowManager 又是在 attach 方法中,通过 mWindow 的 setWindowManager 来初始化的。通过前面的分析可以知道,最终 Window 中的 mWindowManager,是通过 createLocalWindowManager(this) 创建一个新的 WindowManagerImpl 对象,这个对象的 mParentWindow 就是 Activity 中的 mWindow。 (3)基 token 呢?查看添加窗口过程,在调用 addView 方法之前,没有为 token 赋值呢?这里走的是默认的赋值逻辑。而 Activity 对应的 WindowManagerImpl 实例中,mParentWidnow 是其对应的 PhoneWindow。所以在默认检查逻辑中,会走它自己的 PhoneWindow 的 adjustLayoutParamsForSubWindow,把 mAppToken 赋值给 token。 到此为止,type 与 token 都合法了,可以创建窗口了。Window 中的 mAppToken 在 Activity 的 attach 方法中已经通过调用 Window 的 setWindowManager 赋值好了,就是 Activity 中的 mToken 的值。

Dialog 对应的应用窗口的创建

在我们的印象中,Dialog 是个子窗口。但是看 Dialog 的源码,我们会发现 Dialog 的类型是 TYPE_APPLICATION,应用窗口。跟 Activity 对应的窗口一样,Dialog 有一个 PhoneWindow 的实例。在 Dialog 的构建方法中,会通过 PolicyManager.makeNewWindow(mContext)生成一个 Window 对象,其实就是 PhoneWindow 对象,一点跟 Activity 创建应用窗口很像。有一点不同的是,在调用 Window 的 setWindowManager 时,参数 appToken 与 appName 传递都是 null。而 Activity 是把自己的 mToken 给赋值过去的。

接下来,当应用程序调用 show 方法时,Dialog 就会显示出来,由此可见,把 View 添加到窗口的过程应该是在 show 方法中执行的。

分析到这里,有几点疑问需要回答 (1)Dialog的type 是 TYPE_APPLICATION,表示应用窗口。这个应用窗口跟 Activity 对应的应用窗口是什么不一样呢?在分析代码时,发现 Dialog 的 PhoneWindow 实例,在调用 setWindowManger 时,给参数 appToken 传递的是 null,也就是 PhoneWindow 对象的 mAppToken 是空的。而 Activity 的 PhoneWindow 的 mAppToken 等于其 mToken,一定不是空的。 (2) Activity 的 WindowManager 是通过 Context 的 getSystemService 来获取的。而且最终也是通过这个 WindowManger 来执行 addView 操作。这个 WindowManger 的实例,到底是如何创建的呢? (3) Dialog 在 addView 前,也没有为 token 赋值。显然 token 的值需要走默认逻辑来赋值。而默认逻辑跟 WindowManager 实例中的 mParentWindow 有很大的关系的。显然 Dialog 中的 mWindowManager 的 mParentWindow 不能为空,否则 Dialog 的 token 就没有赋值,创建窗口时会抛异常的。还有一个重点哦,Dialog 是应用窗口类型,token必须是 Activity 的 mToken 哦。说到这里,你是不是已经明白呢?如果 mWindowManager 就是 Activity 中的 mWindowManager,一切问题都解决了。Dialog 的构建方法里,要传递一个 Context,平常我们传递 Activity 的实例,看看 Activity 的 getSystemService 的实现。如果传递一个 Application 的 Context 会有什么结果呢?

3.2 子窗口的创建

对于 WmS 来讲,无论什么样的窗口创建,最终都是通过 WindowManagerImpl 的 addView,来添加一个 View。只是对于不同类型的窗口,type 不同,token 的要求也有所不同。创建窗口的过程是本质是一样。常用的子窗口,有 PopupWindow,ContextMenu,OptionMenu。我们从具体的子窗口出发,来分析其中的差异,以及实现的原理。

PopupWindow

分析 PopupWindow 发现,它跟 Dialog 不同,并没有一个 Window 对象,在 invokePopup 方法中,直接把 mPopupView 通过 WindowManager 添加为一个窗口。

其 WindowManager.LayoutParams 是通过 createPopupLayout(anchor.getWindowToken())来初始化的,其 type 类型是 WindowManager.LayoutParams.TYPE_ APPLICATION_ PANEL,属于子窗口类型。因此其 token 是取自外部传递的。无论是通过 showAsDropDown(anchor.getWindowToken() ),还是通过 showAtLocation 始终都要给 PopupWindow 设置一个 token 的。

这里也总结一下,PopUpWindow 的显示: (1)Type类型是TYPE_APPLICATION_ PANEL. (2)其 mWindowManager 是通过 Context 的 getSystemService 来获取的。而 Context 有可能是通过其构建方法传递过来的,或者通过传递的 contentView 去取的。 (3)其 token 是在 addView 前赋值好的。无论是 showAtDropDown 还是 showAtLocation,都需要传递一个 View anchor 进来,token 直接取的 anchor.getWindowToken。View 的 getWindowToken 返回的是 ViewRootImpl 的 AttachInfo 中的 mWindowToken.

ContextMenu (情景菜单)

ContextMenu 是 Android 的一个标准交互,一般是长按一个 View 时,可以显示当前的情景菜单。当我们没有自己处理长按事件时,Android 默认在长按操作时,会调用 showContextMenu(),而 View 的 showContextMenu 又会不断调用 showContextMenuForChild,如果它有 Parent 的话,

对于具有 PhoneWindow 的窗口而言,View 树的根视图是 DecorView,所以最终都会调用 DecorView 的同名方法,其处理如下:

情景菜单真正显示的地方,应该是在 ContextMenuBuilder 的 show 方法里的,看到了吧,这里会调用 originalView 的 createContextMenu方法,而 originalView 就是被长按的 View 呀。

接着,调用 MenuDialogHelper 的 show 方法,来完成创建窗口的操作,到这里是不是对于情景菜单的显示逻辑很清楚了。

我们再来看看 View 的 createContextMenu 做了什么事情,调用本身的 onCreateContextMenu,以及调用 mParent 的 createContextMenu,就这样一直遍历下去。由此可见,父视图的的情景菜单项会出现在每个一个子视图中。

总结来看,情景菜单本质上是一个 Dialog (这里要注意,这个 Dialog 的窗口类型,被赋值为:WindowManager.LayoutParams.TYPE_ APPLICATION_ATTACHED_DIALOG),Framework 的 UI 框架,提供了一种便利的方式借大家添加菜单项。

总结一下,ContextMenu 的创建: (1)本质是一个 Dialog,只是 type 修改为TYPE_APPLICATION_ATTACHED_DIALOG,为子窗口类型; (2)Dialog 的 Context 是取了 menu.getContext(),而 mMenu本 质应该是 DecorView。这个 DecorView 的 context 自然是 PhoneWindow 的 context了。如果长按的是 Activity 对应窗口中的View,自然 Context 是 Activity 了。 (3)final MenuDialogHelper helper = mContextMenu.show(originalView, originalView.getWindowToken()); 可以看到传递的 token 是被按下的 View 的 WindowToken,这个就是一个 IWindow 对象。 (4)其实只要 token 有被赋予正确的值,这里 Context 本身是什么类型,并不重要。无论是什么类型的 Context,获取 WindowService 时,都会返回一个 WinderManagerImpl 的实例。由于 token 已经被正确赋值了,就不需要 mParentWindow 的默认逻辑来赋值了。

OptionMenu (选项菜单)

选项菜单一般是用户按下”Menu”键后弹出的菜单,要启动 OptionMenu,一种是按下“Menu”键,另一种是调用 openOptionsMenu 方法。对于通用的窗口来说,所有的用户消息都由窗口中的视图来处理的,但是对于具有 Window 对象(即 PhoneWindow 对象)的窗口来说,它已经帮窗口处理了”Menu”按键消息。具体在 PhoneWindow 的 onKeyDown 方法中,会调用 onKeyDownPanel,在 onKeyUp 中会调用 onKeyUpPanel。最终来讲,要显示 OptionMenu 都是通过调用 openPanel 来实现的。

分析 openPanel 的实现,我们知道 OptionMenu 并没有 PhoneWindow 对象,仅仅是一个 View,通过 PanelFeatureState 来管理的。显示 OptionMenu 所用到的 WindowManager 其实就是当前调用 openPanel 的 PhoneWindow 的 mWindowManager,跟 Dialog 一样,在执行 addView 过程中,OptionMenu的WindowManager.LayoutParams 的 tokey 会被赋值为 Activity 的 mAppToken。当窗口被真正添加时,会调整为对应的 ViewRootImpl 的 W 类对象。

要显示 OptionMenu,本质就是更新 PanelFeatureState 中的内容,Window.Callback 定义了一些,专门用来准备 Optionmenu 用的,以及响应 OptionMenu 的操作用的。通过 WindowCallback 的这些接口,Android Framework 把显示选项菜单的流程自己处理了,同时具体显示菜单内容的权限交给了 Activity,这就是我们在实现过程中,只需要重载这些接口就能实现显示选项菜单的原因所在了。而且 OptionMenu 的根视图也是 DecorView,

总结一下,OptionMenu 的创建: (1)其 type 类型是TYPE_ APPLICATION_ ATTACHED _ DIALOG,属于子窗口. (2) 由于 OptionMenu 是在 PhoneWindow 的 openPanel 中添加窗口的,WindowManager 直接取了 PhoneWindow的WindowManager. PhoneWindow 中的 WindowManager 是通过((WindowManagerImpl)wm).createLocalWindowManager(this)来创建 的,可见其 mParentWindow 就是当前 PhoneWindow. (3) 显示之前,没有明确为 token 赋值。在 addView 中会走默认的赋值逻辑,最终会调用当前 PhoneWindow 本身的 adjustLayoutParamsForSubWindow。类型是子窗口,会取 PhoneWindow 的 DecorView 的 windowToken 赋值给 token。DecorView 的 WinodwToken 就是 IWindow 的实例。

总结一下子窗口的创建: (1)type 的取值在 [FIRST_SUB_ WINDOW, LAST_ SUB_ WINDOW]; (2)token 必须是父窗口的 IWindow 对象,父窗口必须存在的; (3)当父窗口不可见时,子窗口也不可见;

思考一下,在 Activity 的 onCreate 方法中,可以创建普通的 Dialog 并显示。但是创建一个子窗口类型的 Dialog,并显示吗?

3.3 系统窗口的创建

系统窗口分4类: (1)TYPE_TOAST,不需要声明权限,也不需要 token; (2)第二类,不需要声明权限,但是需要 token,而且对 token 有一定的要求; (3)第三类,需要 android.Manifest.permission.SYSTEM_ALERT_WINDOW 权限; (4)第四类,需要 android.Manifest.permission.INTERNAL_SYSTEM _WINDOW 权限;

Toast 窗口创建

分析一下 Toast 窗口的创建,代码如下:

3.4 窗口的移除

任何一个窗口类型,窗口的移除都是通过调用 WindowManager 的 removeView 来完成的,具体流程如下:

ViewRootImpl 在收到要删除窗口的命令后,会执行以下操作,详细见源码分析: (1)判断是否可以立即删除窗口,否则会等下次 UI 操作时执行;

(2)确认需要删除窗口时,会执行 doDie 方法,通过 dispatchDetachedFromWindow 通知 View 树,窗口要被删除了;

(3)dispatchDetachedFromWindow 执行以下操作 1、通过 dispatchDetachedFromWindow,通知 View 树,窗口已经移除了,你们已经 detach from window 了。 2、把窗口对应的 HardRender, Surface 给释放了; 3、通过 mWindowSession,通知 WmS,窗口要移除了,WmS 会把跟这个窗口相关的 WindowState,以及 WindowToken 给移除,同时更新其它窗口的显示 4、 通知 Choreographer,这个窗口不需要显示了,跟这个窗口相关的一些UI刷新操作,可以取消了。

(4)当根 View 收到 dispatchDetachedFromWindow 调用后,会遍历View树中的每一个 View,把这个通知传递下来。这样 View 的 mAttachInfo 会清除了,reset 为 null了。

//ViewRootImpl.java
boolean die(boolean immediate) {
//如果需要立即移除,立即执行doDie
if (immediate && !mIsInTraversal) {
    doDie();
    return false;
}
if (!mIsDrawing) {
    destroyHardwareRenderer();
} else {
   mWindowAttributes.getTitle());
}
//要不然,通过Handler发个消息,一会执行吧
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
// 在doDie中,会通过dispatchDetachedFromWindow,通知View树,窗口已经移除了
void doDie() {
...
synchronized (this) {
    if (mRemoved) {
        return;
    }
    mRemoved = true;
    if (mAdded) {
        dispatchDetachedFromWindow();
    }
    ...
}
...
}
//dispatchDetachedFromWindow 执行以下操作
//(1) 通过dispatchDetachedFromWindow,通知View树,窗口已经移除了,你们已经detach from window了。
//(2) 把窗口对应的HardRender, Surface给释放了;
//(3) 通过mWindowSession,通知WmS,窗口要移除了,WmS会把跟这个窗口相关的WindowState,以及WindowToken给移除,同时更新其它窗口的显示
//(4) 通知Choreographer,这个窗口不需要显示了,跟这个窗口相关的一些UI刷新操作,可以取消了。
void dispatchDetachedFromWindow() {
if (mView != null && mView.mAttachInfo != null) {
    mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
    mView.dispatchDetachedFromWindow();
}

...
destroyHardwareRenderer();
setAccessibilityFocus(null, null);
mView.assignParent(null);
mView = null;
mAttachInfo.mRootView = null;
mSurface.release();
...
try {
    mWindowSession.remove(mWindow);
} catch (RemoteException e) {
}
...
unscheduleTraversals();
}

四、总结

这里我们总结一下,Android 中有关窗口的相关内容:

(1)在 Window System 中,分为两部分的内容,一部分是运行在系统服务进程(WmS 所在进程)的 WmS 及相关类,另一部分是运行在应用进程的 WindowManagerImpl, WindowManagerGlobal,ViewRootImpl 等相关类。WmS 用 WindowState 来描述一个窗口,而应用进程用 ViewRootImpl,WindowManager.LayoutParms 来描述一个窗口的相关内容。

(2)对于 WmS 来讲,窗口对应一个 View 对象,而不是 Window 对象。添加一个窗口,就是通过 WindowManager 的 addView 方法。同样的,移除一个窗口,就是通过 removeView 方法。更新一个窗口的属性,通过 updateViewLayout 方法。

(3)Window 类描述是一类具有某种通用特性的窗口,其实现类是 PhoneWindow。Activity 对应的窗口,以及 Dialog 对应的窗口,会对应一个 PhoneWindow 对象。PhoneWindow 类把一些操作的统一处理了,例如长按,按”Back”键等。

(4)Android Framework 把窗口分为三种类型,应用窗口,子窗口以及系统窗口。不同类型的窗口,在执行添加窗口操作时,对于 WindowManager.LayoutParams 中的参数 token 具有不同的要求。应用窗口,LayoutParams 中的 token,必须是某个有效的 Activity 的 mToken。而子窗口,LayoutParams 中的 token,必须是父窗口的 ViewRootImpl 中的 W 对象。系统窗口,有些系统窗口不需要 token,有些系统窗口的 token 必须满足一定的要求。

(5)只能通过 Context.getSystemServer 来获取 WindowManager(即获取一个 WindowManagerImpl 的实例)。如果这个 context 是 Activity,则直接返回了 Activity 的 mWindowManager,其 WindowManagerImpl.mParentWindow 就是这个 Activity 本身对应的 PhoneWindow。如果这个 context 是 Application,或者 Service,则直接返回一个 WindowManagerImpl 的实例,而且 mParentWindow 为 null。

(6)在调用 WindowManagerImpl 的 addView 之前,如果没有给 token 赋值,则会走默认的 token 赋值逻辑。默认的 token 赋值逻辑是这样的,如果 mParentWindow 不为空,则会调用其 adjustLayoutParamsForSubWindow 方法。在 adjustLayoutParamsForSubWindow 方法中,如果当前要添加的窗口是,应用窗口,如果其 token 为空,则会把当前 PhoneWindow 的 mToken 赋值给 token。如果是子窗口,则会把当前 PhonwWindow 对应的 DecorView 的 mAttachInfo 中的 mWindowToken 赋值给 token。而 View 中的 AttachInfo mAttachIno 来自 ViewRootImpl 的 mAttachInfo。因此这个 token 本质就是父窗口的 ViewRootImpl 中的 W 类对象。

如果您觉得我们的内容还不错,就转发到朋友圈,和小伙伴一起分享吧~


本文系腾讯Bugly独家内容,转载请在文章开头显眼处注明注明作者和出处“腾讯Bugly(http://bugly.qq.com)”

腾讯Bugly 最专业的质量跟踪平台

为您定期分享应用崩溃解决方案

原文发布于微信公众号 - 腾讯Bugly(weixinBugly)

原文发表时间:2016-02-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏玩转全栈

flutter接入现有的app详细介绍

接入的方式,我是参考的官方的介绍文档,我这里尝试的是android的接入方式,还算比较顺利。

6493
来自专栏指尖下的Android

菜鸡的MVP架构漫谈

相信大家在网上看过关于MVP架构的博客数不胜数,至于MVP到底是什么,也不需要我再从百度百科复制一遍了,通俗的说MVP就是解决Model和View的耦合,没有使...

862
来自专栏酷玩时刻

统一管理项目中的接口回调

无论是 Java Web 开发还是 Android 开发我们都避免不了异步处理业务的逻辑。有异步的出现一定就有异步结果(接口)的回调。你之前是不是有为每一个异步...

924
来自专栏程序员互动联盟

【专业技术】Android内存泄漏简介

存在问题: 不少人认为JAVA程序,因为有垃圾回收机制,应该没有内存泄露。 解决方案: 其实如果我们一个程序中,已经不再使用某个对象,但是因为仍然有引用指向它...

2733
来自专栏知识分享

android 之TCP客户端编程

吸取教训!!!本来花了5个小时写完了,没想到,,,因为没点上面的自动保存查看一下,全没了,重新写呗 关于网络通信:每一台电脑都有自己的ip地址,每台电脑上的网络...

3738
来自专栏QQ音乐技术团队的专栏

[Android] Toast问题深度剖析(二)

题记 Toast 作为 Android 系统中最常用的类之一,由于其方便的api设计和简洁的交互体验,被我们所广泛采用。但是,伴随着我们开发的深入,Toast ...

1.7K9
来自专栏Android相关

AAC---LiveData

LiveData是一个与Activity/Fragment生命周期相关(lifecycle-aware)的Observer类。而这种相关性(awareness ...

1022
来自专栏developerHaoz 的安卓之旅

Android Volley 源码解析(三),图片加载的实现

在上一篇文章中,我们一起深入探究了 Volley 的缓存机制,通过源码分析对缓存的工作原理进行了了解,这篇文章将带大家一起探究「Volley 图片加载的实现」,...

982
来自专栏Android知识点总结

O1-开源框架使用之EventBus

说明使用POSTING,发布与订阅在同一个线程,也就是子线程,更新UI会崩 说明使用MAIN,不管发布者在哪,订阅者都在main线程,可更新UI,但不能耗时操...

1012
来自专栏潇涧技术专栏

Art of Android Development Reading Notes 8

《Android开发艺术探索》读书笔记 (8) 第8章 理解Window和WindowManager

861

扫码关注云+社区

领取腾讯云代金券