Android疑难杂症之——Toast引起的BadTokenException

3,851 阅读2分钟

1.现象分析

当我们将targetSDK升级到26以上后,发现项目中报告了很多BadTokenException异常,查看堆栈几乎都与Toast有关:

crash堆栈

通过堆栈查看源码知道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堆栈:

复现的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

参考

Android 7.X Toast Bug

关于Android7.x系统Toast显示异常BadTokenException解决方案

Toast的BadTokenException