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);
}
这里有个关键点:mService 是 INotificationManager 的 Binder 代理。它指向的是 SystemServer 进程中的 NotificationManagerService(简称 NMS)。
为什么不是直接调 WMS?
因为 Toast 的调度策略在 NMS 里。NMS 维护了一个 Toast 队列,同一时间只允许一个 Toast 显示。你连续点 10 次,它会排队,一个一个弹。
我在项目中遇到过一个问题:快速点击按钮弹出 Toast,结果有的 Toast 没显示出来。后来查源码才发现,NMS 对同一个包名的 Toast 做了限制——队列里最多只能有 50 个待处理的 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 请求后,会做几件事:
- 检查 token:Toast 的 token 来自 NMS,WMS 会验证这个 token 是否有效。
- 分配 Surface:为这个 Window 创建对应的 Surface,用于后续渲染。
- 确定 Z-order:
TYPE_TOAST的层级在TYPE_APPLICATION之上,但在TYPE_SYSTEM_ALERT之下。 - 添加到 Window 列表:WMS 内部维护了一个
mWindowMap,所有 Window 都在这里。
这里有个细节:Toast 的 Window 没有输入通道。因为 FLAG_NOT_TOUCHABLE 和 FLAG_NOT_FOCUSABLE 的组合,WMS 在创建 InputChannel 时会直接跳过。所以 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 会做以下操作:
- 从
mWindowMap中移除该 Window。 - 销毁对应的 Surface。
- 触发 relayout,更新屏幕显示。
然后 cancelToast 通知 NMS:这个 Toast 已经结束了,可以处理下一个了。
所以整个流程就是:显示 → 定时 → 移除 → 通知 NMS → 下一个 Toast 出队。
五、核心流程图:Toast 的完整生命周期
下面这张图,我画了 Toast 从创建到销毁的完整流程。你可以对照着看:
六、避坑指南:我曾经踩过的 Toast 坑
讲几个我实际遇到过的坑,希望能帮你省点时间:
Android 早期版本,Toast 必须在主线程调用。虽然高版本放宽了限制,但 TN.show() 内部还是依赖主线程的 Handler。我建议你始终在主线程调用 Toast.show()。
如果你在 Activity 销毁后还弹 Toast,可能会遇到 WindowManager$BadTokenException。因为 Toast 的 token 已经失效了。解决办法:使用 ApplicationContext 创建 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 会崩溃?
答案都在上面了。