1.现象分析
当我们将targetSDK升级到26以上后,发现项目中报告了很多BadTokenException异常,查看堆栈几乎都与Toast有关:
通过堆栈查看源码知道Toast是通过内部类TN的handleShow()
方法来展示浮窗,而这个方式是可能会抛出WindowManager.BadTokenException异常的,虽然api26之后google对这个异常进行了捕获,使其不至于造成应用crash,但在26之前并没有做任何处理:
在api26之前(特别是26)的机器上有一个稳定复现的路径,在主线程调用Toast的show方法后,阻塞3s左右就会抛出上面的BadTokenException异常并导致crash:
QQToast.makeText(this, "哈哈哈", Toast.LENGTH_SHORT).show();
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
这是可以得到crash堆栈:
2.解决方案
那么在api26之前,我们可以模仿Android8中google针对这个异常的处理方式,通过反射自定义一个Handler的代理,使其捕获这个异常,从而保证应用不会因此而crash
private static class HandlerProxy extends Handler {
private Handler mHandler;
public HandlerProxy(Handler handler) {
this.mHandler = handler;
}
@Override
public void handleMessage(Message msg) {
try {
mHandler.handleMessage(msg);
} catch (Throwable throwable) {
GLog.e(TAG, "toast error: " + throwable.getMessage());
}
}
}
首先定义个Handler的代理,主要用来对Toast中TN的Handler做一个封装
下面通过反射的方式对Toast中TN的Handler做处理:
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
try {
/**
* 获取mTN对象
* 并获取它的class类型
*/
Class<Toast> clazzToast = Toast.class;
Field fieldTN = clazzToast.getDeclaredField("mTN");
fieldTN.setAccessible(true);
Object objTn = fieldTN.get(toast);
Class clazzTn = objTn.getClass();
/**
* 获取TN中的mHandler对象
* 然后用我们自定义的HandlerProxy类包裹它
* 使得它能捕获异常
*/
Field fieldHandler = clazzTn.getDeclaredField("mHandler");
fieldHandler.setAccessible(true);
fieldHandler.set(objTn, new HandlerProxy((Handler) fieldHandler.get(objTn)));
} catch (Throwable throwable) {
GLog.e(TAG, "hack toast handler error: " + throwable.getMessage());
}
}
代码注释写的比较明白,也不难。其实这里就主要是模仿8.0的处理方式来捕获了这个BadTokenException
参考