崩溃详情
尝试复现
通过崩溃信息从网上找到的一些论述,发现这个问题是因为主线程被阻塞了,而 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/ToastUtils,框架内部已经处理了这个问题,调用者无需关心此问题。
使用框架后,可以这么写
ToastUtils.show("666");
还是一句代码,就问你 6 不 6
问题总结
问题描述:Toast 在主线程阻塞情况下会导致 WindowToken 失效,从而导致应用崩溃
涉及范围:所有 Android 版本为 7.1 的用户,并且项目中使用了原生 Toast 的地方都有可能触发崩溃
解决方案:不直接使用原生 Toast,而使用第三方 Toast 框架
作者:Android轮子哥
链接:https://www.jianshu.com/p/437f473017d6
关注我获取更多知识或者投稿