24、Toast 的 Window 机制:Toast 的显示与隐藏是如何通过 WMS 实现的?

Toast 这个组件,大家天天用。但说实话,很多人对它的理解就停留在「弹个提示」的层面。它到底是怎么显示出来的?又是怎么消失的?跟 WMS 有什么关系?

嗯,今天我们就来扒一扒 Toast 的 Window 机制。我个人习惯把这类问题拆成三个环节:申请显示 → 添加 Window → 定时移除。咱们一个一个看。

一、Toast 的「显示」请求是怎么走到 WMS 的?

你调用 Toast.makeText().show() 的时候,背后发生了什么?

先看一段简化后的核心流程:

// Toast.java (简化)
public void show() {
    // 1. 获取 TN 对象(Binder 代理)
    TN tn = mTN;
    // 2. 通过 INotificationManager 跨进程调用
    mService.enqueueToast(pkg, tn, mDuration);
}

这里有个关键点:mServiceINotificationManager 的 Binder 代理。它指向的是 SystemServer 进程中的 NotificationManagerService(简称 NMS)。

为什么不是直接调 WMS?

因为 Toast 的调度策略在 NMS 里。NMS 维护了一个 Toast 队列,同一时间只允许一个 Toast 显示。你连续点 10 次,它会排队,一个一个弹。

我在项目中遇到过一个问题:快速点击按钮弹出 Toast,结果有的 Toast 没显示出来。后来查源码才发现,NMS 对同一个包名的 Toast 做了限制——队列里最多只能有 50 个待处理的 Toast。超过的会被丢弃。

注意:NMS 的 enqueueToast 方法内部会检查当前队列长度。如果超过 50,直接 return,不会报错,也不会显示。这就是「Toast 丢失」的常见原因之一。

二、NMS 如何通知 WMS 添加 Window?

NMS 拿到请求后,会调用 TN.show()。这个 TN 是什么?它是一个 Binder 对象,运行在 你的应用进程 里。

看代码:

// NotificationManagerService.java (简化)
void enqueueToast(...) {
    // 跨进程回调 TN.show()
    tn.show(IBinder token);
}

然后 TN.show() 里做了什么?

// Toast.TN.java (简化)
public void show(IBinder token) {
    // 1. 获取 WindowManager
    mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    // 2. 创建 Toast 的 View(就是那个黑底白字的布局)
    View view = mNextView;
    // 3. 设置 LayoutParams
    WindowManager.LayoutParams params = new WindowManager.LayoutParams();
    params.type = WindowManager.LayoutParams.TYPE_TOAST;
    params.token = token;
    params.flags = FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE;
    // 4. 添加 Window
    mWM.addView(view, params);
}

看到没?mWM.addView() 最终会走到 WindowManagerGlobal.addView(),然后通过 ViewRootImpl 调用 WMS.addWindow()

所以,Toast 的 Window 类型是 TYPE_TOAST。这个类型有什么特殊之处?

  • 不需要权限:普通应用也能弹 Toast,系统默认允许。
  • 不可聚焦FLAG_NOT_FOCUSABLE 保证它不会抢走输入焦点。
  • 不可触摸FLAG_NOT_TOUCHABLE 保证点击事件穿透到下层。

说白了,Toast 就是一个「看得见但摸不着」的悬浮窗。

三、WMS 如何处理 Toast 的 Window?

WMS 收到 addWindow 请求后,会做几件事:

  1. 检查 token:Toast 的 token 来自 NMS,WMS 会验证这个 token 是否有效。
  2. 分配 Surface:为这个 Window 创建对应的 Surface,用于后续渲染。
  3. 确定 Z-orderTYPE_TOAST 的层级在 TYPE_APPLICATION 之上,但在 TYPE_SYSTEM_ALERT 之下。
  4. 添加到 Window 列表:WMS 内部维护了一个 mWindowMap,所有 Window 都在这里。

这里有个细节:Toast 的 Window 没有输入通道。因为 FLAG_NOT_TOUCHABLEFLAG_NOT_FOCUSABLE 的组合,WMS 在创建 InputChannel 时会直接跳过。所以 Toast 不会接收任何触摸事件。

小技巧:如果你想自定义一个「可点击的 Toast」,可以尝试把 FLAG_NOT_TOUCHABLE 去掉。但要注意,这样会拦截点击事件,影响用户体验。我个人不建议这么做。

四、Toast 的「隐藏」机制:定时器 + 移除 Window

Toast 显示一段时间后会自动消失。这个「一段时间」是怎么控制的?

回到 TN.show() 方法,它内部会启动一个 Handler 延时任务:

// Toast.TN.java (简化)
public void show(IBinder token) {
    // ... 添加 Window ...
    mHandler.postDelayed(mHideRunnable, 
        mDuration == LENGTH_LONG ? 3500 : 2000);
}

这个 mHideRunnable 里做了什么?

private final Runnable mHideRunnable = new Runnable() {
    @Override
    public void run() {
        handleHide();
    }
};

void handleHide() {
    // 1. 移除 View
    mWM.removeViewImmediate(mView);
    // 2. 通知 NMS 已移除
    mService.cancelToast(mPackageName, mTN);
}

mWM.removeViewImmediate() 最终会调用 WMS.removeWindow()。WMS 会做以下操作:

  1. mWindowMap 中移除该 Window。
  2. 销毁对应的 Surface。
  3. 触发 relayout,更新屏幕显示。

然后 cancelToast 通知 NMS:这个 Toast 已经结束了,可以处理下一个了。

所以整个流程就是:显示 → 定时 → 移除 → 通知 NMS → 下一个 Toast 出队

五、核心流程图:Toast 的完整生命周期

下面这张图,我画了 Toast 从创建到销毁的完整流程。你可以对照着看:

Toast 完整生命周期流程图 应用进程 SystemServer 进程 WMS 进程 Toast.makeText().show() TN.show() mWM.addView() mWM.removeView() NMS.enqueueToast() 排队 + 回调 TN NMS.cancelToast() WMS.addWindow() 分配 Surface WMS.removeWindow() 蓝色箭头:显示流程 红色箭头:隐藏流程

六、避坑指南:我曾经踩过的 Toast 坑

讲几个我实际遇到过的坑,希望能帮你省点时间:

坑1:Toast 在子线程不显示

Android 早期版本,Toast 必须在主线程调用。虽然高版本放宽了限制,但 TN.show() 内部还是依赖主线程的 Handler。我建议你始终在主线程调用 Toast.show()

坑2:Activity 销毁后 Toast 崩溃

如果你在 Activity 销毁后还弹 Toast,可能会遇到 WindowManager$BadTokenException。因为 Toast 的 token 已经失效了。解决办法:使用 ApplicationContext 创建 Toast。

坑3:Toast 显示时间不准

LENGTH_SHORT 是 2 秒,LENGTH_LONG 是 3.5 秒。但如果你在 show() 之后立即调用 cancel(),可能不会立即消失。因为 cancel() 也是通过 Handler 发消息的,存在时序问题。

七、总结

Toast 的 Window 机制,说白了就是:

  • 显示:应用进程 → NMS 排队 → TN.show() → WMS.addWindow() → 分配 Surface → 显示
  • 隐藏:Handler 延时 → TN.hide() → WMS.removeWindow() → 销毁 Surface → 通知 NMS

整个过程涉及三个进程(应用、NMS、WMS),通过 Binder 跨进程通信。Toast 的 Window 类型是 TYPE_TOAST,不可聚焦、不可触摸,层级适中。

嗯,理解了这个机制,你就能解释很多奇怪的现象了。比如为什么 Toast 不能点击?为什么快速弹 Toast 会丢失?为什么 Activity 销毁后 Toast 会崩溃?

答案都在上面了。


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