9、WindowManagerService(WMS):窗口管理、输入事件分发、WindowToken与Session
各位同学,今天我们聊一个Android系统里真正“管着屏幕”的大管家——WindowManagerService,简称WMS。说实话,我早年做应用开发时,总觉得WMS离我很远,不就是弹个Dialog、加个悬浮窗嘛?直到后来做系统定制,遇到窗口层级错乱、触摸事件“穿透”到后台应用……我才意识到,不懂WMS,你连Bug都定位不了。
这一章,我会从三个核心维度带你拆解WMS:窗口管理、输入事件分发、以及两个关键概念WindowToken和Session。嗯,内容有点硬,但我会用我踩过的坑帮你把路铺平。
9.1 窗口管理:WMS 的核心职责
WMS 管理着所有应用和系统窗口。说白了,它就是屏幕上的“物业公司”——谁该显示、谁该隐藏、谁在最上面,都由它说了算。
每个窗口在 WMS 内部对应一个 WindowState 对象。我刚开始看源码时,觉得 WindowState 就是存个坐标和大小,后来才发现它承载了太多东西:窗口层级、可见性、焦点、动画状态、输入通道……几乎所有的窗口决策都围绕它展开。
窗口层级(Z-order) 是 WMS 最核心的算法之一。WMS 维护了一个全局的窗口列表,按照 mBaseLayer + mSubLayer 排序。应用窗口的 baseLayer 在 1~99,系统窗口在 1000 以上。嗯,这就是为什么系统状态栏永远盖在你的 Activity 上面。
我记得有一次做车载系统,需要让导航应用的悬浮窗始终显示在第三方应用之上,但又不能遮挡系统状态栏。当时我直接修改了 WindowManager.LayoutParams 的 type,把悬浮窗从 TYPE_APPLICATION_OVERLAY 改成了 TYPE_SYSTEM_ALERT,结果导致权限校验失败。后来才明白,type 不仅决定层级,还关联着权限和 token 校验。这个坑,我印象很深。
9.2 输入事件分发:从触摸到窗口的旅程
你点一下屏幕,事件是怎么找到对应窗口的?这背后是 WMS 和 InputManagerService(IMS)的紧密配合。
IMS 从驱动读取原始事件,然后交给 InputDispatcher。InputDispatcher 会向 WMS 查询当前窗口的命中区域。WMS 根据窗口的 position、bound、以及 Z-order,计算出事件应该投递给哪个 WindowState。这个过程叫 hit test(命中测试)。
// 伪代码:命中测试核心逻辑
WindowState hitTest(int x, int y) {
// 从最上层窗口开始遍历
for (WindowState w : mWindowList.descendingIterator()) {
if (w.isVisible() && w.getFrame().contains(x, y)) {
// 还要检查是否被其他窗口遮挡
if (!w.isObscuredByOtherWindows()) {
return w;
}
}
}
return null; // 没有命中任何窗口
}
你可能会问:如果两个窗口重叠怎么办?WMS 会优先把事件分发给 Z-order 更高的窗口。但如果上层窗口不消费事件(比如它是一个透明的 overlay),事件会“穿透”到下层窗口吗?不会。Android 的设计是:一旦命中某个窗口,事件就由它全权处理,除非它显式调用 dispatchTouchEvent 传递下去。这一点,我在做手势导航时踩过坑——上层窗口拦截了事件,下层应用收不到滑动信号。
避坑指南:我曾经遇到一个 Bug,某个系统窗口明明在最上层,但触摸事件却透传给了后面的 Activity。查了两天才发现,是因为窗口的 flags 没有设置 FLAG_NOT_TOUCHABLE,但它的 InputChannel 被意外关闭了。WMS 在分发事件时发现 InputChannel 无效,就会跳过该窗口,继续向下查找。所以,如果你自定义窗口,一定要确保 InputChannel 正常注册。
9.3 WindowToken:窗口的“身份证”
WindowToken 是什么?我个人的理解是:它是 WMS 用来验证窗口合法性的凭据。每个窗口在添加时,都必须关联一个 WindowToken。Token 由 WMS 统一分配,应用进程不能伪造。
Token 主要分两类:
- 应用 Token(AppWindowToken):对应一个 ActivityRecord。Activity 启动时,AMS 会通知 WMS 创建对应的 AppWindowToken。之后该 Activity 的所有窗口(包括 Dialog、PopupWindow)都必须使用这个 Token。
- 系统 Token:用于系统窗口,比如状态栏、导航栏、输入法窗口等。这些 Token 由系统服务直接创建,普通应用无法获取。
为什么要这么设计?说白了,就是为了防止恶意应用伪造系统窗口。比如,一个恶意应用想弹出一个假的系统对话框来窃取密码,如果没有 Token 校验,它只需要设置 type 为 TYPE_SYSTEM_ALERT 就行。但有了 Token 机制,WMS 会检查:你这个进程有没有对应的系统 Token?没有?直接拒绝添加窗口。
注意:WindowToken 的校验发生在 addWindow 方法中。如果校验失败,WMS 会抛出 BadTokenException。我在做插件化框架时,经常遇到这个异常——因为插件 Activity 的 Token 是宿主伪造的,WMS 不认。解决方案是让插件 Activity 通过宿主的 Token 来添加窗口,或者使用 WindowManagerGlobal 的 addView 方法绕过 Token 校验(但需要系统权限)。
9.4 Session:应用与 WMS 的通信通道
Session 是 WMS 与客户端(应用进程)之间的桥梁。每个应用进程在第一次与 WMS 交互时,会通过 Binder 创建一个 Session 对象。WMS 会为每个 Session 分配一个唯一的 ID,并记录它所属的 UID 和 PID。
Session 的主要职责包括:
- 添加、移除、更新窗口(委托给 WMS 的 addWindow/removeWindow/relayoutWindow)
- 管理窗口的 InputChannel(用于接收输入事件)
- 处理窗口焦点和可见性变化
- 权限校验(比如检查调用者是否有 SYSTEM_ALERT_WINDOW 权限)
我举个例子:当你在 Activity 中调用 getWindow().addView(view, params) 时,最终会通过 Session 的 addToDisplay 方法通知 WMS。WMS 会检查 Session 对应的 UID 是否有权限添加该类型的窗口。如果没有,直接返回错误。
// Session.addToDisplay 简化流程
public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs, ...) {
// 1. 权限检查
int res = mService.checkAddPermission(attrs.type, ...);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
// 2. 创建 WindowToken(如果不存在)
WindowToken token = mService.mWindowMap.get(attrs.token);
if (token == null) {
token = new WindowToken(this, attrs.token, ...);
mService.mWindowMap.put(attrs.token, token);
}
// 3. 创建 WindowState 并添加到列表
WindowState win = new WindowState(this, window, token, ...);
mService.addWindowToList(win);
// 4. 建立 InputChannel
mService.mInputManager.registerInputChannel(win.mInputChannel);
return WindowManagerGlobal.ADD_OKAY;
}
这里有个细节:Session 本身也持有对 WMS 的引用,但 WMS 会定期清理那些已经死亡(进程被杀死)的 Session。如果某个进程的 Session 被清理了,它所有的窗口都会被 WMS 强制移除。嗯,这就是为什么应用崩溃后,它的 Toast 和 Dialog 也会消失的原因。
9.5 三者如何协同工作?
我们用一个实际场景来串一下:用户点击了一个按钮,弹出一个 Dialog。
- 创建 Dialog:应用进程通过 Session 调用 WMS 的
addWindow,传入一个 WindowToken(来自 Activity 的 AppWindowToken)。 - Token 校验:WMS 检查这个 Token 是否存在且有效。如果 Token 对应的 Activity 已经销毁,WMS 会拒绝添加窗口。
- 窗口管理:WMS 创建一个 WindowState,计算它的 Z-order(Dialog 的 type 是 TYPE_APPLICATION,层级在 Activity 之上),然后通知 SurfaceFlinger 更新屏幕合成。
- 输入事件:当用户触摸 Dialog 外部时,InputDispatcher 通过 hit test 发现触摸点落在 Dialog 窗口之外,于是将事件分发给 Dialog 下面的 Activity 窗口。如果 Dialog 设置了
setCanceledOnTouchOutside(true),它会在收到外部触摸事件时自动关闭。
整个过程,Token 保证了窗口的合法性,Session 提供了通信通道,WMS 的窗口管理决定了显示和交互。三者缺一不可。
总结一下:WMS 不是孤立工作的。它依赖 WindowToken 来防止窗口滥用,依赖 Session 来隔离进程,依赖 InputDispatcher 来分发事件。理解这三者的关系,你就能在遇到窗口相关 Bug 时,快速定位是哪个环节出了问题。
好了,这一章的内容就到这里。窗口管理、输入事件、Token 和 Session,每一个点都可以深挖很多细节。我个人建议你去看一下 WMS 的 addWindow 方法源码,大概 300 行左右,但几乎涵盖了所有核心逻辑。看懂了它,你就掌握了 WMS 的半壁江山。
公众号:蓝海资料掘金营,微信deep3321