崩溃详情
尝试复现
- 通过崩溃信息从网上找到的一些论述,发现这个问题是因为主线程被阻塞了,而 Toast 没有及时销毁导致的,那么接下来让我们对它进行复现
为什么出现这个问题,是因为 Toast 的显示是通过 Handler.sendMessage,所以这个操作是异步的,而 Thread.sleep 会阻塞主线程,从而导致 Handler.handleMessage 在接收到消息的时候 WindowToken 已经失效了
经过实际的测试:如果是短吐司,sleep 2000 毫秒的时候还是会抛出异常,sleep 1500 毫秒则不会发生异常;如果是长吐司,sleep 3500 毫秒的时候也是会抛出异常,sleep 3000 毫秒的时候就不会发生异常
由此可见,WindowToken 失效的时间是跟 Toast 的显示时长有关,如果是短吐司,那么 WindowToken 有效时长只能在 2 秒以内;如果是长吐司,那么 WindowToken 的有效时长只能在 3.5 秒以内
- 然后再通过 WindowManager.addView 的时候,它会对 WindowToken 例行检查,如果是失效状态则会抛出异常给上层,而这个机制恰好是 Android 7.1 的时候才有的,谷歌那个时候并没有考虑到对 Toast 的一些处理。因为通过浏览 Android 7.0 和 Android 6.0 的源码,发现谷歌也是没有进行 try 处理,但是崩溃的机型却全是清一色的 Android 7.1
问题排查
- 通过查看这个崩溃都是在 Android 7.1 的机型才会出现,那么我们可以对比 Android 7.1 的源码和 Android 8.0 看看
通过追踪不同 API 等级的源码,发现这个问题在 Android 8.0 上面已经被被修复了
通过查看 Toast 的源码,发现 Toast 其实就是一个 WindowManager,并且通过 Handler 来显示和隐藏。
而产生崩溃的地方是在 handleShow 方法里面
- 而 handleShow 方法是被 Toast 中的名为 TN 静态内部类中的 Handler 对象调用
进行修复
- 那么解决这一问题的方式的思路是,将这个 Handler 对象通过反射获取到,然后使用静态代理的方式对它进行回调并对进行捕获异常
最后经过验证,是 OK 的,已经没有崩溃的问题出现了。
但是新的问题又出现了,我们以前写 Toast 是这样的
Toast.makeText(this, "666", Toast.LENGTH_LONG).show();
- 但是如果为了修复这个崩溃问题,我们需要这样写
Toast toast = Toast.makeText(this, "666", Toast.LENGTH_LONG);
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
try {
// 获取 mTN 字段对象
Field mTNField = Toast.class.getDeclaredField("mTN");
mTNField.setAccessible(true);
Object mTN = mTNField.get(toast);
// 获取 mTN 中的 mHandler 字段对象
Field mHandlerField = mTNField.getType().getDeclaredField("mHandler");
mHandlerField.setAccessible(true);
final Handler mHandler = (Handler) mHandlerField.get(mTN);
// 偷梁换柱
mHandlerField.set(mTN, new Handler() {
@Override
public void handleMessage(Message msg) {
// 捕获这个异常,避免程序崩溃
try {
mHandler.handleMessage(msg);
} catch (WindowManager.BadTokenException ignored) {}
}
});
} catch (IllegalAccessException | NoSuchFieldException ignored) {}
}
toast.show();
这样写感觉心好累,不想这样写,有没有一种方式可以一劳永逸?
答案当然是有了,使用第三方 Toast 封装的框架:https://github.com/getActivity/Toaster,框架内部已经处理了这个问题,调用者无需关心此问题。
使用框架后,可以这么写
Toaster.show("666");
- 还是一句代码,就问你 6 不 6
问题总结
问题描述:Toast 在主线程阻塞情况下会导致 WindowToken 失效,从而导致应用崩溃
涉及范围:所有 Android 版本为 7.1 的用户,并且项目中使用了原生 Toast 的地方都有可能触发崩溃
解决方案:不直接使用原生 Toast,而使用第三方 Toast 框架