第八章:WindowManagerService(WMS)——窗口系统的幕后大管家
说实话,WMS 是我在 Android Framework 里最敬畏的模块之一。它不像 AMS 那样天天跟四大组件打交道,也不像 PMS 那样管着安装卸载。但你想啊,屏幕上每一个像素的呈现,每一次触摸事件的传递,背后都是 WMS 在调度。我当年第一次看 WMS 源码时,差点被它的复杂度劝退。但啃下来之后,整个 Android 的图形体系就豁然开朗了。
这一章,我们重点聊四个核心话题:WMS 怎么启动的、窗口怎么添加和删除、Z-order 怎么排、输入事件怎么分发。嗯,都是硬骨头,但啃完你会觉得值。
8.1 WMS 启动流程:从 SystemServer 到窗口管家
WMS 的启动,其实发生在 SystemServer 的 startOtherServices() 方法里。我习惯把这个过程拆成三步:
- 创建 WMS 实例——调用
WindowManagerService.main() - 注册到 ServiceManager——让其他进程能通过 Binder 调用
- 初始化显示与输入系统——关联 DisplayManager 和 InputManager
核心代码长这样:
// 在 SystemServer.java 中
wm = WindowManagerService.main(context, inputManager,
mDisplayManagerService.getDisplayManager(),
mPolicyManager.makeNewWindowPolicy(),
mActivityManagerService.mFactoryTest,
mActivityManagerService.mSystemReady);
ServiceManager.addService(Context.WINDOW_SERVICE, wm);
这里有个细节:WindowManagerPolicy 是在 WMS 启动前就创建好的。它负责定义窗口的行为策略,比如状态栏多高、导航栏怎么显示。我遇到过一个问题:某次定制 ROM 时,状态栏高度没在 Policy 里配对,结果全屏应用底下总有一条黑边。排查了半天,最后发现是 Policy 的 getSystemDecorRect 返回值不对。
displayReady() 和 systemReady()。这两个方法分别处理屏幕信息初始化和系统就绪后的窗口策略调整。如果你在开机阶段遇到窗口显示异常,优先检查这两个方法是否被正确调用。
8.2 Window 添加与删除:应用窗口的“生老病死”
每个应用窗口的诞生,都是从 addWindow() 开始的。说白了,就是应用通过 Binder 告诉 WMS:“嘿,我要显示一个窗口,这是我的 LayoutParams。” WMS 收到请求后,会做一系列检查:
- 权限检查——比如系统窗口需要
SYSTEM_ALERT_WINDOW权限 - Token 验证——窗口必须关联一个有效的
WindowToken - 层级分配——根据 type 和 requestCode 计算 Z-order
添加成功后,WMS 会创建一个 WindowState 对象。这个对象就是窗口在 WMS 里的“身份证”,记录了窗口的所有状态:位置、大小、可见性、动画状态等等。
删除窗口的逻辑在 removeWindow() 里。流程相对简单:
- 标记窗口为移除状态
- 触发窗口动画(如果有)
- 从
mWindowMap中移除 - 通知 SurfaceFlinger 释放对应的 Surface
我曾经踩过一个坑:在 removeWindow() 里直接调用了 Surface.release(),结果导致 ANR。原因是 Surface 的释放必须在主线程外进行,否则会阻塞 UI 绘制。后来我改成通过 MessageQueue 异步释放,问题就解决了。
InputChannel 的注销。否则会出现“幽灵触摸”——窗口已经没了,但触摸事件还在分发。我见过一个案例,就是因为 unregisterInputChannel 没调用,导致用户点击空白区域却触发了已关闭应用的响应。
8.3 Window 层级管理(Z-order):谁在上面,谁在下面
Z-order 说白了就是窗口的堆叠顺序。Android 里窗口分三大类:
| 类型 | Type 范围 | 典型窗口 |
|---|---|---|
| 应用窗口 | 1 ~ 99 | Activity 主窗口 |
| 子窗口 | 1000 ~ 1999 | PopupWindow、Dialog |
| 系统窗口 | 2000 ~ 2999 | 状态栏、导航栏、Toast |
Z-order 的计算逻辑在 assignLayers() 里。核心思路是:先按 type 分层,再在同一层内按 requestCode 排序。系统窗口永远在最上层,应用窗口在中间,子窗口依附于父窗口。
嗯,这里有个容易忽略的点:窗口的 Z-order 不是静态的。比如输入法窗口弹出时,WMS 会动态调整它的层级,让它盖住当前输入框。调整逻辑在 performLayoutAndPlaceSurfacesLocked() 里,每次窗口状态变化都会触发重新计算。
我建议你调试 Z-order 问题时,用 dumpsys window 命令。它会输出所有窗口的层级信息,包括 mBaseLayer 和 mSubLayer。我曾经靠这个命令定位过一个诡异问题:一个全屏游戏的悬浮窗总是被系统导航栏挡住,最后发现是 type 设成了 TYPE_APPLICATION 而不是 TYPE_SYSTEM_OVERLAY。
SurfaceControl.setLayer() 告诉 SurfaceFlinger。但 SurfaceFlinger 还会根据 BufferQueue 的状态做微调。所以有时候你看到窗口层级不对,不一定是 WMS 的问题,也可能是 SurfaceFlinger 那边的同步延迟。
8.4 输入事件分发(InputManager):从手指到窗口的旅程
输入事件分发,是 WMS 里最让我着迷的部分。你想想看,你点一下屏幕,事件要经过多少层才能到达你的应用?
流程大致是这样的:
- InputReader 从驱动读取原始事件
- InputDispatcher 根据窗口的
InputChannel分发事件 - WMS 通过
InputMonitor决定事件该发给哪个窗口 - 应用端的
ViewRootImpl接收事件,交给 View 树处理
这里的关键是 InputMonitor。它维护了一个 mInputWindowHandles 列表,记录了所有可接收输入的窗口。每次窗口添加或删除时,WMS 都会更新这个列表,然后通知 InputDispatcher 重新计算焦点。
焦点计算逻辑在 updateInputWindows() 里。说白了就是:找到 Z-order 最高且可触摸的窗口,把事件发给它。但有个例外——如果窗口设置了 FLAG_NOT_TOUCHABLE,事件会穿透到下层窗口。
我曾经遇到一个经典问题:一个全屏的透明悬浮窗,挡住了底层应用的点击事件。排查后发现,悬浮窗没有设置 FLAG_NOT_TOUCHABLE 和 FLAG_NOT_FOCUSABLE。加上这两个 flag 后,事件就正常穿透了。
adb shell dumpsys input 可以查看当前所有窗口的输入通道状态。重点关注 FocusedWindow 和 TouchableRegions。如果发现焦点窗口不对,多半是 WMS 的 updateInputWindows 没被正确触发。
嗯,说到输入事件,还有一个容易被忽略的点:事件注入。WMS 提供了 injectInputEvent() 方法,用于模拟输入。自动化测试框架就是靠这个实现的。但要注意,注入事件需要 INJECT_EVENTS 权限,普通应用拿不到。
知识体系总览
下面这张图,是我梳理的 WMS 核心模块关系。你看一眼,应该能对整体有个把握:
这张图里,WMS 是中心,四个子模块各司其职。底部三个服务是 WMS 的“左膀右臂”。你仔细看,输入分发模块和 InputManagerService 之间是虚线,说明 WMS 并不直接调用 IMS,而是通过 InputMonitor 做桥接。这个设计很巧妙——解耦了窗口管理和输入处理。
好了,WMS 的核心内容就这些。说实话,每次讲到这里我都觉得意犹未尽。但篇幅有限,咱们先消化这些。下一章我们会深入 SurfaceFlinger,看看窗口最终是怎么画到屏幕上的。
公众号:蓝海资料掘金营,微信deep3321