Android-Window机制
Android-Window机制
1. Window的表现形式
1.1 什么是Window
在大部分 PC 操作系统中,Window 指的是一个可视化的窗口区域。例如在 MacOS、Linux、Windows 系统中,可视化内容的 Window 通常包含(并非必须)「关闭按钮」、「最小化」、「最大化」,这个 Window 是由操作系统分配的且具有 UI(例如边框、按钮、标题等),限定了内容显示的区域,可视化的内容在渲染时只需要相对给定的 Window 渲染和绘制,因此提供给了用户自由操作的空间。
1.2 Android中的Window
在 Android 中,Window 更多只是一种抽象的概念:一组 View 的集合,因为 Android 中的 Window 并没有实际的 UI。在源码中有 Window
, PhoneWindow
, ViewManager
, WindowManager
, WindowManagerImpl
, WindowManagerGlobal
等与 Window 相关的类,但这些都不是 Window 实体。
Android 中 Window 和 View 的关系就像「组织」和「个体」的关系,一群相似的个体可以组成一个集体并对外具有一定属性、功能,但「组织」只是一个概念上的集合,并不是一个实际存在的东西。
2. Window属性
Window 有 3 个重要属性:
- type
- flags
- solfInputMode
2.1 Type
由于 Android 中 Window 是对一组 View 集合的描述,不同应用有不同的 Window,同一个应用也可能有多个 Window,所以需要用一种优先级表示 Window 的显示层级,在 Android 中使用 type
表示 Window 的类型,实际上是通过 type
的大小表示 Window 的优先级,值越大则在屏幕上显示位置越上层,会覆盖低层级的 Window:
1 | public interface WindowManager extends ViewManager { |
2.2 Flags
Flags 用于指定 Window 在不同场景下的处理方式,包括样式(全屏、背景等)、显示模式(是否允许锁屏、是否允许超出屏幕等)、触摸事件处理逻辑(是否接受触摸响应、是否将外部触摸事件传递到下层窗口等):
1 | public interface WindowManager extends ViewManager { |
2.3 SoftInputMode
SoftInputMode 决定了 Window 在弹出软键盘时的逻辑,实际上软键盘也是一种 Window,但因为比较特殊所以专门用了一个属性来控制:
1 | public interface WindowManager extends ViewManager { |
3. 创建Window
3.1 指定用于创建Window的View
在创建一个 Window 时,例如在 Activity 手动调用 getWindowManager().addView(...)
,或是 Dialog#show
,本质上都是调用了 WindowManager 实现类的 WindowManagerImpl#addView
方法,实际上 WindowManagerImpl
内的方法都只是对入参做了一些校验和预处理,最终都是通过桥接模式调用单例的 WindowManagerGlobal
实现的:
1 | public final class WindowManagerImpl implements WindowManager { |
WindowManagerGlobal#addView(...)
主要做了五件事:
- 校验入参;
- 如果
parentWindow != null
说明需要创建 SubWindow,则 SubWindow 需要继承部分 parentWindow 的属性; - 创建 ViewRootImpl;
- 把用于创建 Window 的 View、ViewRootImpl、应用到 Window 的 LayoutParams 分别存入三个列表,这三个列表总是等长的,三列表中同一下标对应的三个对象均指向了同一个 Window;
- 将用于创建 Window 的 View 以及 LayoutParams 传入 ViewRootImpl,由 ViewRootImpl 实际创建 Window。
这里也能看出,每次创建 Window 时都会先创建一个 ViewRootImpl,而真正创建 Window 的步骤其实就在 ViewRootImpl 内,并不是 WindowManager 也不是 PhoneWindow,ViewRootImpl 与创建的 Window 是一一对应的。
3.2 ViewRootImpl创建Window
ViewRootImpl 在构造方法中通过单例的 WindowManagerGlobal 获取了一个单例的 IWindowSession 对象,用于在 ViewRootImpl#setView(...)
中真正创建 Window:
1 | public final class ViewRootImpl implements ViewParent { |
这也能看出,WindowManagerGlobal 和 IWindowSession 都是应用内单例的;
WindowManagerGlobal 是一个工具类,用于维护和提供一系列与 Window 相关的资源,例如在
addView(...)
中就创建并维护了 View、ViewRootImpl、LayoutParams,而getWindowSession(...)
则维护并提供 IWindowSession 对象,但 WindowManagerGlobal 本身并不承担创建 Window 的责任,是一个纯粹的工具类。IWindowSession 包含了一系列与 WindowManagerService 的交互逻辑:
1
2
3
4
5
6
7
8
9class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
final WindowManagerService mService;
public int addToDisplay(...) {
return mService.addWindow(...);
}
}
所以,关于 Window、PhoneWindow、ViewManager、WindowManager、WindowManagerImpl、WindowManagerGlobal、ViewRootImpl 的关系可以总结如下:
- 首先在
Activity#attach(...)
阶段先创建new PhoneWindow(...)
; - 然后通过
PhoneWindow#setWindowManager(...)
的内部创建出来一个 WindowManagerImpl; - 所有创建 Window 的地方,不论是 Activity、Dialog、还是 PopupWindow,最终都会调用
WindowManagerImpl#addView(...)
; - WindowManagerImpl 则调用单例的
WindowManagerGlobal#addView(...)
; - WindowManagerGlobal 创建
new ViewRootImpl(...)
; - ViewRootImpl 通过
setView(...)
持有了用于创建 Window 的 View,以及指定 Window 属性的 LayoutParams,因此 ViewRootImpl 负责连接 View 和 Window; - ViewRootImpl 从 WindowManagerGlobal 中获取单例的 IWindowSession,调用
IWindowSession#addToDisplay(...)
; - IWindowSession 调用 WMS 完成最终的 Window 创建,因此 IWindowSession 负责连接 ViewRootImpl 和 WMS。
除了创建窗口,WindowManagerImpl 还能通过
updateView(...)
和removeView(...)
更新及移除 Window。
4. PhoneWindow和ViewRootImpl
4.1 PhoneWindow不是Window
通过上文分析会发现,创建 Window 最终都是通过 WindowManagerImpl#addView(...)
实现的,而 WindowManagerImpl 又是在 PhoneWindow#setWindowManager(...)
中创建的,那为什么 PhoneWindow 不是一个实际的 Window 呢?这个问题需要从两个方面考虑:
(1)PhoneWindow 与 Window 并不是一一对应关系:
首先,PhoneWindow 和实际显示在屏幕上的 Window 并不是一一对应的关系。尽管在 Activity 启动流程中创建了一个 new PhoneWindow(...)
,但例如 PopupWindow、或是手动调用 getWindowManager().addView(...)
创建一个 Window 时,都并没有创建 PhoneWindow,而且它们最终都是用这个 Activity 的 WindowManagerImpl 来创建的,与 Activity 创建的 PhoneWindow 并没有直接关系。即便是 Dialog 在构造方法中创建了属于自己的 PhoneWindow 对象,但 show()
的时候用到的 WindowManagerImpl 也是通过 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
从传入的 Context 中获取的,而创建 Dialog 传入的 Context 又几乎肯定是 Activity,所以实际上这些创建 Window 的过程,仅仅只和 WindowManagerImpl 有关,与 PhoneWindow 并没有什么关系。
(2)PhoneWindow 并没有与 WMS 的交互:
Android 对 Window 真正的创建、更新、和移除,都是通过 IWindowSession 调用 WindowManagerService 实现的;反之 WMS 也是通过 IWindowSession 来分配、调度、以及管理实际的 Window。而每个 App 只有一个单例的 IWindowSession,保存在同样单例的 WindowManagerGlobal 中,只有 ViewRootImpl 获取并操作了 IWindowSession,而 ViewRootImpl 又是 WindowManagerImpl 每次调用 addView(...)
时创建的,因此这个与 WMS 交互的过程同样与 PhoneWindow 无关。
4.2 PhoneWindow的意义
上文中提到,Activity 在 attach(...)
时创建了自己的 PhoneWindow,而 Dialog 也在构造方法中创建了自己的 PhoneWindow,乍一看似乎 PhoneWindow 就是一个实际的 Window,当然通过上文也已经知道并非如此。PhoneWindow 与 Window 的关系,非常类似于 Socket 与 TCP / UDP 的关系:
- TCP / UDP 都是实际上的传输层协议,但 Socket 既不参与实际数据协议的转换(传输层)、又不负责维护设备之间的通信信道(会话层,Socket 虽然可以建立连接,但 Socket 只是帮忙发起了请求,本质上这条连接并不是 Socket 维护的,而是操作系统维护的)、也不负责传输数据的编解码(表示层),因为 Socket 并不属于 OSI 模型中的任何一层,Socket 只是对 TCP / UDP 的封装而并不参与到实际网络传输中的缓解,应该把 Socket 看成一个工具。
- 而 PhoneWindow 与 Window 的关系也是同理:
- PhoneWindow 维护了与 Window 相关的资源,例如 WindowManager、Type 属性、Flags 属性、SoftInputMode 属性等;
- 并提供了一系列与 Window 相关的工具方法,例如加载 DecorView、设置 Title、获取实际 Window 的一些状态等;
- 封装了对 DecorView、SubDecor 的加载;
- 当需要改变 Window 的属性时,实际上是把对应的属性设置到 PhoneWindow 中,然后由 ViewRootImpl 请求 WMS 后读取并按照对应的属性设置 Window。
当然上文也提到,Dialog 会在构造方法中创建自己的 PhoneWindow,这是因为 Dialog 是一个很灵活的组件,本质上 Dialog 其实就是另一个 Window,但开发人员对 Dialog 的 Window 有很大的自定义需求,经常需要修改 Window 的属性,因此 Dialog 对创建出来的 Window 资源做了一层封装,放在自己的 PhoneWindow 中,而对于 PopupWindow 或手动调用 getWindowManager().addView(...)
创建的 Window,只是 Google 没有提供获取 Window 并修改属性的 API 而已。
4.3 ViewRootImpl的意义
通过分析 Window 的创建流程可以发现,Window 创建真正相关的其实是 WindowManagerImpl 和 ViewRootImpl,甚至可以说直接相关的只有 ViewRootImpl,因为 DecorView 也是在 ViewRootImpl 中维护的。而上文也提到 Android 中 Window 只是一种概念,Android 中的 Window 是用一组 ViewTree 表示的,因为 ViewTree 具有从上到下的层级关系,所以实际上 Window 可以与 ViewTree 的顶点一一对应,那这个顶点应该是什么呢?
答案就是 ViewRootImpl,而不是 DecorView。尽管在 Activity 启动流程以及 Window 的创建流程中,DecorView 才是实际上 ViewTree 最上层的 View,而且 DecorView 本身也继承自 View,但是 View 最主要的功能还是承担 UI 上的显示,用一个 View 来承担与 Window 的交互从设计模式上是不合理的,而 ViewRootImpl 对上承担了 View 对 Window 的请求,对下承担了 Window 对 View 的事件分发,因此把 ViewRootImpl 看作 ViewTree 的顶点更合适。个人认为,这也是为什么 ViewRootImpl 会实现 ViewParent 接口的原因,这么设计使得 ViewRootImpl 也可以看成 View 的 Parent,例如每一个 View 在需要刷新 UI 的时候都会调用 invalidate()
和 requestLayout()
,最终也都会向上传递到 ViewRootImpl,然后由 ViewRootImpl 统一处理渲染、VSync 信号处理等等逻辑,而 ViewTree 中实际的每个 View 只需要根据 ViewRootImpl 分发的回调处理即可。
5. Window加载时机
5.1 Activity的Window
在 Activity 启动流程 中对 Activity 的启动做了分析,Activity 的 Window 是在 ActivityThread#handleResumeActivity(...)
中调用 wm.addView(decor, l);
创建的:
1 | public final class ActivityThread extends ClientTransactionHandler { |
最终调用了 Activity#performResume(...)
:
1 | public class Activity { |
通过源码可以看出,Activity 在回调 onResume()
生命周期的时候,Window 以及 ViewRootImpl 是还没有创建的!
5.2 创建新Window
上文已经得出结论,Activity 在 onResume()
执行完返回之后,才会执行创建 Window 和 ViewRootImpl 的逻辑;
- 这就带来了一个问题:在 Activity 生命周期的
onResume
回调中,可以创建 Window 吗?例如弹出一个 PopupWindow、或是调用getWindowManager().addView(...)
、或是弹出一个 Dialog? - 答案:
- Dialog 可以。
- PopupWindow 默认情况下不能,需要指定某个范围内的 Type,否则抛出异常。
- 手动调用
getWindowManager().addView(...)
只有在 Window 的 Type 属性在某个范围内才可以,否则抛出异常。
抛出的异常是:android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
,抛出的地方位于:
1 | public final class ViewRootImpl { |
通过上文分析可知,创建一个 Window 的过程为:
1 | public final class WindowManagerImpl implements WindowManager { |
可以看到在 Window#adjustLayoutParamsForSubWindow(LayoutParams)
中对新 Window 的判断有三个大分支,按优先级顺序:
1000 <= Type <= 1999
2000 <= Type <= 2999
1 <= Type <= 99
5.2.1 作为SubWindow时
当新 Window 1000 <= Type <= 1999
时,说明新 Window 是一个 SubWindow,SubWindow 的前提就是已经存在父 Window,因此 Token 就从父 Window 的 DecorView 里面获取:
1 | // getWindowToken() 其实是 View 的方法 |
DecorView#getWindowToken()
获取的实际上是从 View 成员变量 mAttachInfo
获取的,因此就需要找到这个 mAttachInfo
是什么时候被存入 DecorView 的,也就是 dispatchAttachedToWindow(...)
是什么时候调用的:
1 | public final class ActivityThread extends ClientTransactionHandler { |
从源码中可以分析得出:
- WindowManagerGlobal 创建 ViewRootImpl 之后调用
ViewRootImpl#setView(...)
将 DecorView 存入 ViewRootImpl; - ViewRootImpl 在实际开始渲染第一帧的时候才将 AttachInfo 存入 DecorView;
- 这之后
DecorView#getWindowToken()
才不为空。 - 因此当新建的 Window
1000 <= Type <= 1999
时,只有在 DecorView 第一帧渲染之后才能获取到 Token。
5.2.2 作为SystemWindow时
当 2000 <= Type <= 2999
时,说明新 Window 是一个 SystemWindow,则不需要从其他 Window 继承任何属性,也不需要复用其他 Window 的 Token,Android 将以独立生命周期处理这个 Window,在源码中也没有做什么处理,这种情况下是不会抛出异常的。
5.2.3 作为ApplicationWindow时
当上述两个条件均不满足时,说明 1 <= Type <= 99
,也即新 Window 是一个 ApplicationWindow,则新 Window 就直接复用当前 Window 的 Token,又因为 WindowManagerGlobal 中调用的是 parentWindow.adjustLayoutParamsForSubWindow(LayoutParams)
,所以新 Window 就是复用的 parentWindow
的 Token,对应在 Activity 中就是 Activity 的 Window 的 Token,再进一步也就是 Activity#attach(...)
阶段从 AMS 传进来的 Token,因此新 Window 在这个阶段(只是预处理 LayoutParams 的阶段,还没有创建 ViewRootImpl)就已经具有了 Token,因此在后续创建 ViewRootImpl 并调用 ViewRootImpl#setView(...)
时也就不会抛出异常。
5.2.4 问题分析
上文 3 个小节已经对新 Window 的 Type 取不同值时,对应 Token 的获取时机做了分析,因此对于 Activity#onCreate()
时是否能创建新 Window,就能通过新 Window 的 Type 取值做判断了:
(1)PopupWindow:
1 | public class PopupWindow { |
可以看到,PopupWindow 在成员变量中已经指定了默认的 Type == 1000
,因此默认情况下的 PopupWindow 在 Activity#onResume()
阶段无法通过 DecorView 获取到 Token,会抛出异常,但可以通过手动设置 popupWindow.setWindowLayoutType(1 ~ 99)
直接复用 Activity 的 Window 的 Token 解决问题。
(2)手动调用 getWindowManager.addView(...)
:
1 | getWindowManager().addView(textView, new WindowManager.LayoutParams( |
由于可以手动指定 Type 属性,因此指定在 1 ~ 99 范围内即可。
(3)Dialog:
1 | public class Dialog { |
可以看到,Dialog 在构造方法中创建了自己的 PhoneWindow,而 Window 默认的 LayoutParams 中 Type == TYPE_APPLICATION == 2
,所以 Dialog 也是直接复用了当前 Window 的 Token。