Android 7.X Toast Bug

错误信息

android.view.WindowManager$BadTokenException: Unable to add window 
-- token android.os.BinderProxy@e428e31 is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:679)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
at android.widget.Toast$TN.handleShow(Toast.java:459)
at android.widget.Toast$TN$2.handleMessage(Toast.java:342)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

复现方式

targetSdk升至26及以上,Android 7.x手机,代码调用toast.show()之后,UI线程立即耗时操作2s以上,即会Crash。

原因分析

Toast最终是通过内部类TN的handleShow()方法展示浮窗:

8.0的toast源码:

public void handleShow(IBinder windowToken) {
        ...
        try {
            mWM.addView(mView, mParams);
            trySendAccessibilityEvent();
        } catch (WindowManager.BadTokenException e) {
            /* ignore */
        }
    }
}

7.x上:

public void handleShow(IBinder windowToken) {
        ...
        mWM.addView(mView, mParams);
        trySendAccessibilityEvent();
    }
}

这里不深究什么会抛出BadTokenException异常,有兴趣可以参考这篇文章

这里handler在处理事件执行addView时,抛出了异常,由于没有try-catch,导致7.x手机上crash。

解决办法

由于涉及跨线程,所以我们不能直接对Toast嵌套try-catch。处理办法就是仿造8.0,在handle时捕获异常。

解决方案即通过反射,替换TN的mHandler为下面我们的代理类,然后在代理类中try-catch

    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 (WindowManager.BadTokenException e) {
                //ignore
            }
        }
    }

Hook方法为:

    public static void hookToast(Toast toast) {
        Class<Toast> cToast = Toast.class;
        try {
            //TN是private的
            Field fTn = cToast.getDeclaredField("mTN");
            fTn.setAccessible(true);

            //获取tn对象
            Object oTn = fTn.get(toast);
            //获取TN的class,也可以直接通过Field.getType()获取。
            Class<?> cTn = oTn.getClass();
            Field fHandle = cTn.getDeclaredField("mHandler");

            //重新set->mHandler
            fHandle.setAccessible(true);
            fHandle.set(oTn, new HandlerProxy((Handler) fHandle.get(oTn)));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

最终调用:

mToast = Toast.makeText(context, message, duration);
hookToast(mToast);
mToast.show();

有兴趣可以自己做下封装。