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。

整个流程可以用下面这张图来概括:

Dialog Window 附着流程 Dialog.show() WindowManagerImpl.addView() ViewRootImpl.setView() WMS.addWindow() 验证 Token 有效性 创建 WindowState 分配 Surface Activity 的 Token Token 传递

5. Dialog 的 Window 类型

Dialog 的 Window 类型是 TYPE_APPLICATION,它的层级在 TYPE_BASE_APPLICATIONTYPE_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 就无法显示。

整个流程可以概括为:

  1. Dialog 创建时,从 Context 获取 WindowManager,并保存 Activity 的 Token。
  2. 调用 show() 时,创建 DecorView,设置 Token,调用 WindowManager.addView()。
  3. WindowManagerImpl 创建 ViewRootImpl,调用 setView()。
  4. ViewRootImpl 通过 Binder 调用 WMS 的 addWindow()。
  5. WMS 验证 Token 有效性,创建 WindowState,分配 Surface。
  6. Dialog 的 Window 正式显示在屏幕上。

嗯,这块内容其实不难,但细节很多。你只要记住 Token 是关键,其他都是围绕 Token 展开的。

核心一句话:Dialog 的 Window 通过 Token 附着到 Activity 上,Token 是 WMS 管理 Window 会话的凭证。


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