一、Activity 与 WindowManager 的“暧昧关系”
说实话,很多 Android 开发者做了两三年,对 Activity 和 WindowManager 的关系还是模模糊糊的。我刚开始接触这块时也踩过坑——总以为 Activity 就是“显示界面”的,其实它只是个生命周期管理者。真正负责把界面画到屏幕上的,是 WindowManager。
那它们到底是什么关系?
简单来说:Activity 是“房主”,WindowManager 是“施工队”。Activity 告诉系统“我要盖个房子”,WindowManager 负责把砖瓦(View)砌好,挂到墙上(屏幕)。
嗯,这里有个关键点:每个 Activity 都对应一个 Window,而这个 Window 的“施工许可证”就是 WindowToken。
核心结论:Activity 通过 WindowManager 添加 DecorView,而 ActivityRecord 与 WindowToken 的关联,决定了这个窗口能否被系统正确管理。
二、Activity 的窗口创建流程:从 startActivity 到 onResume
我们一步步拆解。假设你调用了 startActivity(),系统会经历什么?
2.1 ActivityRecord 的诞生
AMS(ActivityManagerService)收到启动请求后,会创建一个 ActivityRecord。这个对象记录了 Activity 的所有信息:包名、启动模式、Intent 等等。
我个人习惯把 ActivityRecord 看作 Activity 的“身份证”。它不负责显示,只负责记录状态。
2.2 WindowToken 的创建
当 Activity 即将显示时,WMS(WindowManagerService)会为它创建一个 WindowToken。这个 Token 是窗口的“通行证”。没有它,WindowManager 不会受理任何窗口操作。
这里有个细节:WindowToken 的类型是 TYPE_APPLICATION,表示这是一个应用窗口。
2.3 ActivityRecord 与 WindowToken 的绑定
在 Activity 启动过程中,AMS 会调用 WMS.addWindowToken(),将 WindowToken 与 ActivityRecord 关联起来。具体来说:
- ActivityRecord 的
token字段指向对应的 WindowToken - WindowToken 的
appWindowToken字段指向 ActivityRecord
说白了,这就是一个双向指针。系统通过这个关联,知道“哪个窗口属于哪个 Activity”。
避坑指南:我曾经遇到过一个 bug,Dialog 弹出后 Activity 无法响应触摸事件。查了半天发现是 WindowToken 类型设置成了 TYPE_APPLICATION_PANEL,但没有正确关联父窗口的 Token。记住:子窗口必须持有父窗口的 Token,否则事件分发会出问题。
三、DecorView 的添加:View 如何挂到窗口上
Activity 的 onCreate() 里我们调用 setContentView(),但这只是把布局文件解析成 View 树。真正把 View 挂到窗口上,是在 onResume() 之后。
3.1 谁创建了 DecorView?
是 PhoneWindow。每个 Activity 在创建时,都会在 attach() 方法里创建一个 PhoneWindow 对象。PhoneWindow 的 installDecor() 方法会创建 DecorView。
DecorView 是什么?它是整个 Activity 窗口的根 View。你写的布局文件,最终会被塞进 DecorView 的 android.R.id.content 区域。
3.2 添加 DecorView 的时机
Activity 的 handleResumeActivity() 方法中,会调用 WindowManager.addView(decorView, layoutParams)。这一步才是真正的“挂载”。
代码流程大致如下:
// ActivityThread.java
private void handleResumeActivity(ActivityClientRecord r, ...) {
// 1. 执行 onResume
r.activity.performResume();
// 2. 获取 DecorView
View decor = r.activity.getWindow().getDecorView();
// 3. 获取 WindowManager
WindowManager wm = r.activity.getWindowManager();
// 4. 添加 View 到窗口
wm.addView(decor, r.activity.getWindow().getAttributes());
}
你想想看,为什么不在 onCreate() 里添加?因为那时候 Activity 还没准备好接收用户交互。等到 onResume() 时,Activity 才处于“可见且可交互”的状态。
3.3 WindowManager.addView() 内部做了什么?
这个方法最终会调用到 WindowManagerGlobal.addView(),然后通过 Binder 跨进程通知 WMS。WMS 会做几件事:
- 检查 WindowToken 是否有效
- 分配 Surface(绘图表面)
- 将窗口信息加入窗口列表
- 触发 relayout,计算窗口位置和大小
嗯,这里要注意:addView 是异步的。你调用完这个方法,View 并不会立刻显示在屏幕上。WMS 会在下一帧刷新时处理。
警告:不要在非 UI 线程调用 WindowManager.addView()。虽然它不会直接崩溃,但会导致窗口布局错乱。我见过一个同事在子线程里弹 Toast,结果 Toast 显示位置偏移了半个屏幕。
四、核心关系图:ActivityRecord、WindowToken、DecorView 的三角关系
为了让你更直观地理解,我画了一张图。这张图展示了从 Activity 启动到窗口显示的核心链路。
五、常见问题与避坑指南
5.1 WindowToken 丢失导致崩溃
如果你在 Dialog 关闭后,还尝试通过它的 WindowManager 添加 View,会收到 BadTokenException。这是因为 Dialog 的 WindowToken 已经被 WMS 移除了。
我曾经在 Fragment 的 onDestroyView() 里尝试更新 Dialog 的 UI,结果直接崩溃。解决方案是:在 dismiss 之前,先判断 isShowing()。
5.2 DecorView 的测量时机
很多人在 onCreate() 里获取 DecorView 的宽高,得到的是 0。为什么?因为这时候 DecorView 还没被添加到窗口,没有经过 measure 和 layout。
正确的做法是在 onWindowFocusChanged() 或使用 ViewTreeObserver 监听全局布局变化。
5.3 多窗口模式下的特殊处理
Android 7.0 引入多窗口后,Activity 的窗口大小会动态变化。这时候 WindowToken 依然有效,但 DecorView 的尺寸会重新计算。我建议在 onConfigurationChanged() 里重新获取布局参数。
总结一下:Activity 和 WindowManager 的关系,说白了就是“生命周期管理者”和“界面管理者”的分工。ActivityRecord 是身份证,WindowToken 是通行证,DecorView 是房子本身。这三者缺一不可,任何一个环节出问题,你的界面就会“塌房”。
公众号:蓝海资料掘金营,微信deep3321