26、SystemUI的Toast与Dialog:Toast的自定义窗口,Dialog的WindowToken校验
说实话,Toast和Dialog这两个东西,看起来简单,但坑是真的多。
我早期做系统定制的时候,就因为在Dialog里弹了一个系统级Toast,结果窗口直接崩了。后来一查,原来是WindowToken校验没通过。嗯,今天我们就来彻底聊透这两个组件。
一、Toast的本质:一个带超时机制的窗口
很多人以为Toast是View,其实不是。Toast本质上是一个窗口(Window),只不过它自带了一个自动消失的定时器。
它的核心流程是这样的:
- 应用层调用
Toast.makeText(),传入上下文和文本。 - Toast内部通过
INotificationManager跨进程通知SystemUI。 - SystemUI的
ToastManager负责调度显示。 - 最终通过
WindowManager.addView()添加一个悬浮窗。
说白了,Toast就是SystemUI帮你托管的一个小窗口。它用的窗口类型是 TYPE_TOAST,这个类型有个特殊权限——不需要 SYSTEM_ALERT_WINDOW 权限也能弹。
关键点:Toast的窗口类型是 TYPE_TOAST,优先级低于 TYPE_APPLICATION_OVERLAY,但高于普通应用窗口。
二、自定义Toast窗口:我踩过的坑
我在项目中遇到过这样一个需求:需要弹出一个带图标的Toast,而且图标要动态变化。当时我第一反应是自定义Layout,然后直接 setView()。
代码大概长这样:
Toast toast = new Toast(context);
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.custom_toast, null);
ImageView icon = view.findViewById(R.id.toast_icon);
icon.setImageResource(R.drawable.ic_success);
toast.setView(view);
toast.setDuration(Toast.LENGTH_SHORT);
toast.show();
看起来没问题对吧?但实际跑起来,在某些Android 11以上的设备上,Toast直接不显示。
原因是什么?
Android R之后,Google对Toast做了限制:非系统应用的自定义Toast视图,不能直接使用 setView() 设置复杂布局。系统会检查你的View是否包含 ImageView、Button 等可交互控件,如果有,直接拦截。
避坑指南:我曾经在Android 12上调试了整整两天,才发现是自定义Toast被系统静默丢弃了。解决方案是改用 Snackbar 或者直接使用系统默认Toast样式。
如果你非要自定义,可以试试这个思路:
- 使用
Toast.makeText()获取默认Toast - 通过
getView()拿到默认的TextView - 在TextView外面包一层自定义布局(但这个方法在Android 13上也被限制了)
嗯,说白了,Google就是不希望你乱改Toast的样子。
三、Dialog的WindowToken校验:一个绕不开的门槛
Dialog和Toast最大的区别是什么?
Dialog需要WindowToken。没有Token,Dialog弹不出来。
WindowToken是什么?你可以把它理解成「窗口的通行证」。每个Activity在创建时,WMS(WindowManagerService)会分配一个唯一的Token。Dialog必须持有这个Token,才能被添加到窗口层级中。
我画了一张图,帮你理解这个流程:
四、Token校验失败的典型场景
我遇到过最典型的场景是这样的:
某个后台服务收到推送后,想弹一个Dialog提醒用户。开发同学直接写了:
// ❌ 错误写法
Dialog dialog = new Dialog(getApplicationContext());
dialog.setContentView(R.layout.dialog_notification);
dialog.show();
结果呢?直接崩溃:
android.view.WindowManager$BadTokenException:
Unable to add window -- token null is not valid; is your activity running?
为什么会这样?因为 ApplicationContext 没有Activity的WindowToken。Dialog需要依附在Activity的窗口上,没有Token,WMS直接拒绝。
我的建议:Dialog一定要用Activity的Context来创建。如果确实需要在非Activity场景弹窗,可以考虑使用 TYPE_APPLICATION_OVERLAY 类型的窗口,但需要 SYSTEM_ALERT_WINDOW 权限。
五、SystemUI中的Dialog特殊处理
在SystemUI里,Dialog的使用方式又不太一样。因为SystemUI本身是一个系统进程,它拥有更高的权限。
SystemUI中的Dialog通常使用 mContext 直接创建,这个Context是SystemUI的ApplicationContext。但SystemUI的Dialog能正常显示,原因在于:
- SystemUI的窗口类型通常是
TYPE_SYSTEM_DIALOG或TYPE_NAVIGATION_BAR - 这些类型不需要Activity的Token,WMS会直接放行
- 但要注意,这类窗口会覆盖在所有应用之上
我整理了一个对比表格,方便你理解:
| 特性 | 普通App Dialog | SystemUI Dialog | Toast |
|---|---|---|---|
| 窗口类型 | TYPE_APPLICATION | TYPE_SYSTEM_DIALOG | TYPE_TOAST |
| 需要Token | ✅ 必须 | ❌ 不需要 | ❌ 不需要 |
| 权限要求 | 无 | 系统权限 | 无 |
| 生命周期 | 跟随Activity | 跟随SystemUI | 定时自动消失 |
| 可自定义程度 | 高 | 高 | 低(受限) |
六、实战:如何优雅地处理Dialog的Token校验
如果你在开发中遇到了 BadTokenException,别慌。我一般这样处理:
// 安全弹窗的通用写法
public static void showDialogSafely(Activity activity, Dialog dialog) {
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
// 嗯,Activity已经没了,别弹了
return;
}
try {
dialog.show();
} catch (BadTokenException e) {
// 捕获异常,避免崩溃
Log.w("DialogUtils", "WindowToken无效,无法显示Dialog");
}
}
这个写法虽然简单,但很实用。我在多个项目中都用到了它。
另外,如果你用的是 DialogFragment,它会自动处理Token问题,因为 DialogFragment 的生命周期和FragmentManager绑定在一起。我个人比较推荐这种方式。
核心总结:
- Toast是带定时器的窗口,自定义受限
- Dialog必须持有有效的WindowToken
- SystemUI的Dialog不需要Token,但需要系统权限
- 弹窗前一定要检查Activity状态
好了,关于Toast和Dialog的窗口机制,今天就聊到这里。这些东西看起来基础,但真正遇到问题的时候,还是挺折腾人的。希望今天的分享能帮你少踩几个坑。
公众号:蓝海资料掘金营,微信deep3321