Dialog 的 Window 机制:Dialog 的 Window 是如何附着到 Activity 上的?
说实话,Dialog 这个组件,大家天天用。但你真的清楚它的 Window 是怎么挂到 Activity 上的吗?
我记得刚入行那会儿,遇到一个诡异的 Bug:Dialog 在横竖屏切换时闪退。查了半天,发现是 Window 的附着时机出了问题。从那以后,我就把 Dialog 的 Window 机制彻底啃了一遍。
今天,我就把这块硬骨头给你拆开揉碎讲清楚。
1. Dialog 的本质:一个独立的 Window
很多人以为 Dialog 是 Activity 的一部分。其实不是。
Dialog 本质上是一个独立的 Window。它有自己的 ViewRootImpl,有自己的 Surface。说白了,它和 Activity 是平级的,只不过在视觉上看起来像是浮在 Activity 上面。
核心结论:Dialog 的 Window 是一个独立的 Window,类型为 TYPE_APPLICATION,它通过 Token 机制附着到 Activity 的 Window 上。
2. 附着过程:Token 是关键
Dialog 要附着到 Activity 上,靠的是什么?Token。
Token 是 WindowManagerService 用来标识一个 Window 会话的凭证。Activity 在创建时,会从 WMS 拿到一个 Token。Dialog 要显示,就必须持有这个 Token。
我们来看一下 Dialog 的构造函数:
// Dialog.java
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
// ...
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// 关键:获取 Activity 的 Window 的 Token
final Window window = getWindow();
window.setWindowManager(mWindowManager, null, null);
// ...
}
嗯,这里要注意。Dialog 在创建时,会从 Context 中获取 WindowManager。如果这个 Context 是 Activity 的,那么 WindowManager 内部就已经包含了 Activity 的 Token。
我们再看一下 Window.setWindowManager() 的内部实现:
// Window.java
public void setWindowManager(WindowManager wm, IBinder appToken, String appName) {
// ...
mWindowManager = ((WindowManagerImpl) wm).createLocalWindowManager(this);
// 关键:将 Activity 的 Token 保存下来
mAppToken = appToken;
}
Token 被保存在 Window 的 mAppToken 字段中。当 Dialog 调用 show() 时,这个 Token 会被传递给 WMS,用于验证 Dialog 是否有权限附着到该 Activity 上。
3. show() 方法:Window 的添加过程
Dialog 的 show() 方法,是 Window 真正被添加到 WMS 的入口。
// Dialog.java
public void show() {
// ...
mDecor = mWindow.getDecorView();
WindowManager.LayoutParams l = mWindow.getAttributes();
// 设置 Token
l.token = mWindow.mAppToken;
mWindowManager.addView(mDecor, l);
// ...
}
这里有几个关键点:
- 获取 DecorView:Dialog 的 DecorView 是在
show()时创建的,而不是在构造函数中。 - 设置 Token:将 Activity 的 Token 设置到 LayoutParams 中。
- 调用 addView:最终通过 WindowManagerImpl 的 addView 方法,将 DecorView 添加到 WMS。
避坑指南:我曾经在非 UI 线程调用 Dialog.show(),结果直接崩溃。因为 WindowManager.addView() 必须在主线程调用。后来我养成了习惯:所有 UI 操作都 post 到主线程。
4. WindowManagerImpl 的 addView 流程
WindowManagerImpl 的 addView 方法,最终会调用到 ViewRootImpl 的 setView 方法。我们来看一下关键链路:
// WindowManagerImpl.java
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
// ...
ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
// ...
}
ViewRootImpl 的 setView 方法会通过 Binder 调用 WMS 的 addWindow 方法。WMS 会检查 Token 是否有效,然后为 Dialog 创建一个 WindowState,并分配 Surface。
整个流程可以用下面这张图来概括:
5. Dialog 的 Window 类型
Dialog 的 Window 类型是 TYPE_APPLICATION,它的层级在 TYPE_BASE_APPLICATION 和 TYPE_APPLICATION_PANEL 之间。
我们来看一下 Window 类型的层级关系:
| Window 类型 | 层级范围 | 说明 |
|---|---|---|
| TYPE_BASE_APPLICATION | 1 | 基础应用窗口 |
| TYPE_APPLICATION | 2 | 普通应用窗口(Dialog 使用) |
| TYPE_APPLICATION_PANEL | 3 | 面板窗口(PopupWindow 使用) |
| TYPE_APPLICATION_MEDIA | 4 | 媒体窗口 |
| TYPE_APPLICATION_SUB_PANEL | 5 | 子面板窗口 |
注意:Dialog 的 Window 类型是 TYPE_APPLICATION,这意味着它和 Activity 的 Window 在同一个层级。但 Dialog 的 Window 会覆盖在 Activity 的 Window 之上,因为 Dialog 的 Window 是在 Activity 的 Window 之后添加的。
6. Dialog 的 Window 与 Activity 的 Window 的关系
Dialog 的 Window 和 Activity 的 Window 之间,有几个重要的关系:
- Token 绑定:Dialog 的 Window 持有 Activity 的 Token,WMS 通过 Token 来管理它们的关系。
- 生命周期绑定:当 Activity 销毁时,Dialog 的 Window 也会被移除。因为 Token 失效了。
- 输入事件分发:Dialog 的 Window 会拦截输入事件,Activity 的 Window 无法收到事件。
我记得有一次,我在 Dialog 中使用了 setCanceledOnTouchOutside(true),结果点击 Dialog 外部时,Dialog 消失了,但 Activity 的点击事件却没有触发。这是因为 Dialog 的 Window 拦截了事件,但没有传递给 Activity。
7. 避坑指南:Dialog 的 Window 泄漏
Dialog 的 Window 泄漏是一个常见问题。我曾经在一个项目中,因为忘记在 Activity 销毁时关闭 Dialog,导致 Window 泄漏。
为什么会泄漏?
因为 Dialog 的 Window 是通过 WindowManager 添加到 WMS 的。如果 Activity 销毁了,但 Dialog 没有调用 dismiss(),那么 WMS 中仍然保留着 Dialog 的 WindowState。这个 WindowState 持有 Activity 的 Context 引用,导致 Activity 无法被 GC 回收。
避坑指南:我建议在 Activity 的 onDestroy() 中,主动调用 dialog.dismiss()。或者使用 DialogFragment,它会自动管理生命周期。
8. 总结
Dialog 的 Window 机制,说白了就是通过 Token 将 Dialog 的 Window 附着到 Activity 的 Window 上。Token 是 WMS 用来验证权限的凭证,没有 Token,Dialog 就无法显示。
整个流程可以概括为:
- Dialog 创建时,从 Context 获取 WindowManager,并保存 Activity 的 Token。
- 调用
show()时,创建 DecorView,设置 Token,调用 WindowManager.addView()。 - WindowManagerImpl 创建 ViewRootImpl,调用 setView()。
- ViewRootImpl 通过 Binder 调用 WMS 的 addWindow()。
- WMS 验证 Token 有效性,创建 WindowState,分配 Surface。
- Dialog 的 Window 正式显示在屏幕上。
嗯,这块内容其实不难,但细节很多。你只要记住 Token 是关键,其他都是围绕 Token 展开的。
核心一句话:Dialog 的 Window 通过 Token 附着到 Activity 上,Token 是 WMS 管理 Window 会话的凭证。
公众号:蓝海资料掘金营,微信deep3321