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 (内部持有)

嗯,这里要注意:AppWindowTokenWindowToken 的子类。它专门为 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 传到客户端的?

流程大概是这样的:

  1. AMS 启动 Activity,调用 WMS.addAppToken,创建 AppWindowToken
  2. AMS 把 Token 的 Binder 引用传给 ActivityThread
  3. ActivityThread 创建 Activity 实例,把 Token 存到 Activity.attach() 的参数里
  4. Activity 内部通过 getWindowManager() 拿到 WindowManagerImpl,它持有 Token
  5. 调用 addView 时,Token 被封装到 ViewRootImpl 的请求中,发给 WMS

嗯,这个链路挺长的。但好处是:Token 始终是同一个 Binder 对象,WMS 能精确识别。

SVG:Token 与窗口分组关系图

Token 与窗口分组关系 WMS 内部 WindowToken (token=0x1234) AppWindowToken (token=0x5678, activity=com.example.MainActivity) 主窗口 (DecorView) 子窗口 (Dialog) 弹出窗口 (PopupWindow) 同一个 AppWindowToken 下的所有窗口,共享生命周期和焦点管理

Token 的「坑」与避坑指南

我这些年见过不少 Token 相关的问题,挑几个典型的说说:

  • BadTokenException:最常见。Activity 已经销毁了,但还有线程在调 addView。解决方案是:在 onDetachedFromWindowonDestroy 里取消所有异步操作。
  • Token 泄露:窗口加了没移除。WMS 里会残留 AppWindowToken,导致内存泄漏。我建议用 LeakCanary 检测,或者自己写个工具类,在 onDestroy 时强制清理。
  • 跨进程 Token 传递:如果你用 WindowManageraddView 跨进程加窗口,Token 必须是 IBinder 类型,且要保证序列化正确。我遇到过传 null 的情况,WMS 直接抛异常。

避坑指南:我曾经在做一个悬浮窗功能时,用了 Application 的 Context 来获取 WindowManager。结果发现 Token 传的是 null,WMS 直接拒绝。后来改成 Activity 的 Context,Token 就对了。记住:只有 Activity 的 Context 才持有有效的 Token。

总结一下

Token 是 WMS 管理窗口的核心凭据。它把同一个 Activity 的窗口分组管理,让生命周期、焦点、层级都井井有条。

理解 Token 的变化时机,能帮你避免很多运行时异常。尤其是 BadTokenException,几乎每个 Android 开发者都遇到过。

嗯,这部分内容就到这里。下一章我们会深入 WindowState,看看 WMS 内部是怎么管理每个窗口的状态的。


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