第八章:WindowManagerService(WMS)——窗口系统的幕后大管家

说实话,WMS 是我在 Android Framework 里最敬畏的模块之一。它不像 AMS 那样天天跟四大组件打交道,也不像 PMS 那样管着安装卸载。但你想啊,屏幕上每一个像素的呈现,每一次触摸事件的传递,背后都是 WMS 在调度。我当年第一次看 WMS 源码时,差点被它的复杂度劝退。但啃下来之后,整个 Android 的图形体系就豁然开朗了。

这一章,我们重点聊四个核心话题:WMS 怎么启动的、窗口怎么添加和删除、Z-order 怎么排、输入事件怎么分发。嗯,都是硬骨头,但啃完你会觉得值。

8.1 WMS 启动流程:从 SystemServer 到窗口管家

WMS 的启动,其实发生在 SystemServer 的 startOtherServices() 方法里。我习惯把这个过程拆成三步:

  1. 创建 WMS 实例——调用 WindowManagerService.main()
  2. 注册到 ServiceManager——让其他进程能通过 Binder 调用
  3. 初始化显示与输入系统——关联 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 返回值不对。

个人经验:WMS 启动后,会立即调用 displayReady()systemReady()。这两个方法分别处理屏幕信息初始化和系统就绪后的窗口策略调整。如果你在开机阶段遇到窗口显示异常,优先检查这两个方法是否被正确调用。

8.2 Window 添加与删除:应用窗口的“生老病死”

每个应用窗口的诞生,都是从 addWindow() 开始的。说白了,就是应用通过 Binder 告诉 WMS:“嘿,我要显示一个窗口,这是我的 LayoutParams。” WMS 收到请求后,会做一系列检查:

  • 权限检查——比如系统窗口需要 SYSTEM_ALERT_WINDOW 权限
  • Token 验证——窗口必须关联一个有效的 WindowToken
  • 层级分配——根据 type 和 requestCode 计算 Z-order

添加成功后,WMS 会创建一个 WindowState 对象。这个对象就是窗口在 WMS 里的“身份证”,记录了窗口的所有状态:位置、大小、可见性、动画状态等等。

删除窗口的逻辑在 removeWindow() 里。流程相对简单:

  1. 标记窗口为移除状态
  2. 触发窗口动画(如果有)
  3. mWindowMap 中移除
  4. 通知 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 命令。它会输出所有窗口的层级信息,包括 mBaseLayermSubLayer。我曾经靠这个命令定位过一个诡异问题:一个全屏游戏的悬浮窗总是被系统导航栏挡住,最后发现是 type 设成了 TYPE_APPLICATION 而不是 TYPE_SYSTEM_OVERLAY

核心要点:Z-order 的最终决定权在 SurfaceFlinger。WMS 只是计算出每个窗口的“建议层级”,然后通过 SurfaceControl.setLayer() 告诉 SurfaceFlinger。但 SurfaceFlinger 还会根据 BufferQueue 的状态做微调。所以有时候你看到窗口层级不对,不一定是 WMS 的问题,也可能是 SurfaceFlinger 那边的同步延迟。

8.4 输入事件分发(InputManager):从手指到窗口的旅程

输入事件分发,是 WMS 里最让我着迷的部分。你想想看,你点一下屏幕,事件要经过多少层才能到达你的应用?

流程大致是这样的:

  1. InputReader 从驱动读取原始事件
  2. InputDispatcher 根据窗口的 InputChannel 分发事件
  3. WMS 通过 InputMonitor 决定事件该发给哪个窗口
  4. 应用端的 ViewRootImpl 接收事件,交给 View 树处理

这里的关键是 InputMonitor。它维护了一个 mInputWindowHandles 列表,记录了所有可接收输入的窗口。每次窗口添加或删除时,WMS 都会更新这个列表,然后通知 InputDispatcher 重新计算焦点。

焦点计算逻辑在 updateInputWindows() 里。说白了就是:找到 Z-order 最高且可触摸的窗口,把事件发给它。但有个例外——如果窗口设置了 FLAG_NOT_TOUCHABLE,事件会穿透到下层窗口。

我曾经遇到一个经典问题:一个全屏的透明悬浮窗,挡住了底层应用的点击事件。排查后发现,悬浮窗没有设置 FLAG_NOT_TOUCHABLEFLAG_NOT_FOCUSABLE。加上这两个 flag 后,事件就正常穿透了。

调试技巧:adb shell dumpsys input 可以查看当前所有窗口的输入通道状态。重点关注 FocusedWindowTouchableRegions。如果发现焦点窗口不对,多半是 WMS 的 updateInputWindows 没被正确触发。

嗯,说到输入事件,还有一个容易被忽略的点:事件注入。WMS 提供了 injectInputEvent() 方法,用于模拟输入。自动化测试框架就是靠这个实现的。但要注意,注入事件需要 INJECT_EVENTS 权限,普通应用拿不到。

知识体系总览

下面这张图,是我梳理的 WMS 核心模块关系。你看一眼,应该能对整体有个把握:

WMS 核心模块关系图 WindowManagerService 窗口管理 addWindow / removeWindow 层级管理 Z-order / assignLayers 输入分发 InputMonitor / 焦点 策略管理 WindowManagerPolicy SurfaceFlinger InputManagerService DisplayManagerService 虚线表示依赖关系,实线表示直接调用 WMS 通过 SurfaceControl 与 SurfaceFlinger 通信

这张图里,WMS 是中心,四个子模块各司其职。底部三个服务是 WMS 的“左膀右臂”。你仔细看,输入分发模块和 InputManagerService 之间是虚线,说明 WMS 并不直接调用 IMS,而是通过 InputMonitor 做桥接。这个设计很巧妙——解耦了窗口管理和输入处理。

好了,WMS 的核心内容就这些。说实话,每次讲到这里我都觉得意犹未尽。但篇幅有限,咱们先消化这些。下一章我们会深入 SurfaceFlinger,看看窗口最终是怎么画到屏幕上的。


公众号:蓝海资料掘金营,微信deep3321