26、WindowToken 与 AppWindowToken:Token 的作用、窗口分组管理、Activity 生命周期中的 Token 变化
说实话,WindowManager 里最绕的概念,就是 Token 了。
我刚接触这部分源码时,第一反应是:这玩意儿不就是个标识符吗?跟 ID 差不多吧?
后来踩了坑才明白——Token 远不止是个 ID。它是窗口分组的核心,是 Activity 和 Window 之间的纽带,也是系统管理窗口层级的关键凭据。
Token 到底是什么?
简单说,Token 是一个 Binder 对象。它由 WMS 创建,然后分发给客户端。
每个窗口在添加时,都必须携带一个 Token。WMS 拿到 Token 后,会检查它是否合法、是否过期。说白了,Token 就是窗口的「入场券」。
核心要点:没有 Token,窗口就加不上去。这是 WMS 的安全机制,防止恶意应用乱弹窗口。
Token 的继承关系是这样的:
WindowToken
└── AppWindowToken
└── ActivityRecord (内部持有)
嗯,这里要注意:AppWindowToken 是 WindowToken 的子类。它专门为 Activity 服务。
窗口分组管理:Token 如何把窗口「捆」在一起?
你想想看,一个 Activity 可能有好几个窗口:主窗口、子窗口、弹出菜单……这些窗口怎么管理?
答案就是 Token。
同一个 Activity 的所有窗口,都使用同一个 AppWindowToken。WMS 看到这些窗口的 Token 相同,就知道它们是一伙的。
分组的好处很明显:
- 统一生命周期:Activity 销毁时,WMS 直接找到对应的
AppWindowToken,然后移除它下面所有窗口。不用一个个遍历。 - 层级控制:同组窗口的 Z-order 是连续的,不会出现「A 的窗口插到 B 的窗口中间」这种乱象。
- 焦点管理:WMS 按 Token 组来分配焦点。你点一个窗口,焦点不会跳到另一个 Activity 的窗口上。
我的经验:我曾经遇到过一个 bug——Dialog 弹出来,但焦点却跑到了后面的 Activity 上。查了半天,发现是 Dialog 的 Token 传错了,用了另一个 Activity 的 Token。WMS 认为它们是一组的,焦点自然就乱了。
Activity 生命周期中的 Token 变化
Token 不是一成不变的。它在 Activity 的整个生命周期里,会经历几个关键阶段。
| 阶段 | Token 状态 | 说明 |
|---|---|---|
| Activity 创建 | 生成 AppWindowToken | AMS 通知 WMS 创建 Token,此时窗口还没加 |
| onResume 之后 | Token 激活 | 窗口可以正常添加、显示 |
| onPause | Token 冻结 | 新窗口不能加,但已有窗口还能显示 |
| onStop / onDestroy | Token 移除 | WMS 清理所有关联窗口 |
这里有个细节:AppWindowToken 的创建时机,其实比 Activity 的 onCreate 还要早。
AMS 在启动 Activity 时,会先通知 WMS:「我要开一个 Activity 了,给我准备一个 Token。」WMS 创建好 AppWindowToken,然后 AMS 才去调 Activity.onCreate。
为什么要这么设计?
因为 Activity 在 onCreate 里可能就会调用 setContentView,进而触发 WindowManager.addView。如果 Token 还没准备好,窗口就加不上去了。
注意:Token 的移除时机是 onDestroy 之后。但如果你在 onStop 里调了 finish,Token 可能会提前被标记为「移除中」。这时候再加窗口,会收到 BadTokenException。
Token 的传递链路
Token 是怎么从 WMS 传到客户端的?
流程大概是这样的:
- AMS 启动 Activity,调用
WMS.addAppToken,创建AppWindowToken - AMS 把 Token 的 Binder 引用传给
ActivityThread ActivityThread创建Activity实例,把 Token 存到Activity.attach()的参数里- Activity 内部通过
getWindowManager()拿到WindowManagerImpl,它持有 Token - 调用
addView时,Token 被封装到ViewRootImpl的请求中,发给 WMS
嗯,这个链路挺长的。但好处是:Token 始终是同一个 Binder 对象,WMS 能精确识别。
SVG:Token 与窗口分组关系图
Token 的「坑」与避坑指南
我这些年见过不少 Token 相关的问题,挑几个典型的说说:
- BadTokenException:最常见。Activity 已经销毁了,但还有线程在调
addView。解决方案是:在onDetachedFromWindow或onDestroy里取消所有异步操作。 - Token 泄露:窗口加了没移除。WMS 里会残留
AppWindowToken,导致内存泄漏。我建议用LeakCanary检测,或者自己写个工具类,在onDestroy时强制清理。 - 跨进程 Token 传递:如果你用
WindowManager的addView跨进程加窗口,Token 必须是IBinder类型,且要保证序列化正确。我遇到过传null的情况,WMS 直接抛异常。
避坑指南:我曾经在做一个悬浮窗功能时,用了 Application 的 Context 来获取 WindowManager。结果发现 Token 传的是 null,WMS 直接拒绝。后来改成 Activity 的 Context,Token 就对了。记住:只有 Activity 的 Context 才持有有效的 Token。
总结一下
Token 是 WMS 管理窗口的核心凭据。它把同一个 Activity 的窗口分组管理,让生命周期、焦点、层级都井井有条。
理解 Token 的变化时机,能帮你避免很多运行时异常。尤其是 BadTokenException,几乎每个 Android 开发者都遇到过。
嗯,这部分内容就到这里。下一章我们会深入 WindowState,看看 WMS 内部是怎么管理每个窗口的状态的。