ToastUtil:修复Android 7.x设备Toast显示时抛出的WindowManager$BadTokenException Token失效异常

最近在项目新版本测试中,当在Android 7.x(SDK=24/25)设备上跑Monkey测试APP时,经常报Token失效异常:“android.view.WindowManager$BadTokenException: Unable to add window – token android.os.BinderProxy@ba9eb53 is not valid; is your activity running?”,导致APP出现Crash,直接终止运行:

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@ba9eb53 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:94)
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:158)
at android.app.ActivityThread.main(ActivityThread.java:6175)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:893)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)

一、BadTokenException异常产生原因

       从上面异常堆栈信息可以看出,异常发生在当系统Toast内部类对象TN内部的Handler在收到显示消息Message,进行处理并调用 Toast$TN.handleShow()方法时,Toast$TN.handleShow()方法在不同的Android版本中实现也不一样:
       在Android 7.x版本,handleShow()方法实现如下:

public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                ...
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ....
                mParams.token = windowToken;
                ...
                mWM.addView(mView, mParams);
                ...
            }
        }

       在Android 8.0版本,handleShow()方法实现如下:

public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                ...
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ....
                mParams.token = windowToken;
                ...
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
                ...
            }
        }

        在上面可以看出,Google已经在Android 8.0源码中在调用WindowManagerImpl.addView()前通过使用try-catch捕获WindowManager.BadTokenException异常,修复了该bug,避免APP发生Crash,这也是为什么该异常在Android 7.x设备上频繁出现,而在Android 8.0设备上几乎没有发生的原因。
       在Android 7.x设备上,通常情况下,按照正常的流程,是不会出现这种异常。但是由于在某些情况下, 尤其是在跑Monkey测试的时候,Android 进程某个 UI 线程的某个消息阻塞,导致 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息之后,迟迟没有执行,导致超时引起NotificationManager超时检测机制删除WMS 服务中的 Token 记录,很容易导致该异常发生,具体Toast显示流程源码分析以及异常产生原因可以阅读下面QQ音乐技术团队的分析文章[Android] Toast问题深度剖析(一)

二、如何修复该异常

       正如上面分析,异常发生在当系统Toast内部类对象TN内部的Handler在收到显示消息Message,进行处理并调用 Toast$TN.handleShow()方法时,handleShow()方法是Toast内部类TN的方法,我们无法通过直接继承Toast重写handleShow()方法来捕获该异常,不过通过异常堆栈信息可知,在调用Toast$TN.handleShow()前,会先调用Toast$TN$Hanlder.handleMessage(),而调用Toast$TN$Hanlder.handleMessage()前,一定会先调用Handler.dispatchMessage() 方法,我们可以创建一个安全的Handler装饰器,通过重写Handler.dispatchMessage() 方法捕获抛出的异常即可,装饰器Handler实现代码如下:

   /**
     * Safe outside Handler class which just warps the system origin handler object in the Toast.class
     */
    private static class SafelyHandlerWarpper extends Handler {
        private Handler originHandler;

        public SafelyHandlerWarpper(Handler originHandler) {
            this.originHandler = originHandler;
        }

        @Override
        public void dispatchMessage(Message msg) {
            // 在此处使用try-catch捕获BadTokenException,当内部Hanlder发生异常,外部SafelyHandlerWarpper可以捕获,
            // 防止应用Crash
            try {
                super.dispatchMessage(msg);
            } catch (Exception e) {
                Log.e(TAG, "Catch system toast exception:" + e);
            }
        }

        @Override
        public void handleMessage(Message msg) {
            // 需要委托给原Handler执行
            if (originHandler != null) {
                originHandler.handleMessage(msg);
            }
        }
    }

       然后,我们需要使用定义的SafeHandlerWarpper对象去包装 Toast$TN$Hanlder, 然后通过反射去替换 Toast$TN$Hanlder对象,具体请见如下hookToast()方法:

	private static final String FIELD_NAME_TN = "mTN";
    private static final String FIELD_NAME_HANDLER = "mHandler";
   /**
     * Hook Toast,修复在7.x手机上跑monkey的时候,Toast低概率出现BadTokenException的异常
     *
     * @param toast
     */
    private static void hookToast(Toast toast) {
        if (!isNeedHook()) {
            return;
        }
        try {
            if (!sIsHookFieldInit) {
                sField_TN = Toast.class.getDeclaredField(FIELD_NAME_TN);
                sField_TN.setAccessible(true);
                sField_TN_Handler = sField_TN.getType().getDeclaredField(FIELD_NAME_HANDLER);
                sField_TN_Handler.setAccessible(true);
                sIsHookFieldInit = true;
            }
            Object tn = sField_TN.get(toast);
            Handler originHandler = (Handler) sField_TN_Handler.get(tn);
            sField_TN_Handler.set(tn, new SafelyHandlerWarpper(originHandler));
        } catch (Exception e) {
            Log.e(TAG, "Hook toast exception=" + e);
        }
    }

        我们仅需要选择在Android 7.x设备(SDK版本为24或25)上使用自定义Handler装饰器SafelyHandlerWarpper去hook系统的Toast$TN$Hanlder对象,如果项目在其他Android版本上也出现该异常,可以根据自己项目需要去添加即可,具体请见如下isNeedHook()方法:

  /**
     * Check if Toast need hook,only hook the device 7.x(api = 24/25)
     *
     * @return true for need hook to fit system bug,false for don't need hook
     */
    private static boolean isNeedHook() {
        return Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1 ||
                Build.VERSION.SDK_INT == Build.VERSION_CODES.N;
    }

        最后,在我们调用Toast.show()准备显示Toast前,调用hookToast()方法即可:

       if (mToast == null) {
                mToast = Toast.makeText(context, text, duration);
            } else {
                mToast.setText(text);
                mToast.setDuration(duration);
            }
            hookToast(mToast);
            mToast.show();
        }

三、如何复现以及检测是否修复

        正如上面所说,当UI线程阻塞时,很容易导致该问题产生,我们可以通过在调用Toast.show()方法后,在主线程中调用Thread.sleep()阻塞主线程,导致WMS Token超时失效,就可以在Android 7.x设备上复现该Exception,如下代码所示

                Toast.makeText(MainActivity.this,"I am origin Toast without fix",Toast.LENGTH_SHORT).show();
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e){
                    e.printStackTrace();
                }

        下面通过Demo来复现以及验证,本例子中,Demo UI设计如下:
在这里插入图片描述
        当点击第一个Button时,我们直接使用系统的Toast来显示Toast:

btnUnfixed.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,"I am origin Toast without fix",Toast.LENGTH_SHORT).show();
                try {
                    // just sleep and block the main thread which will reappear the BadTokenException
                    Thread.sleep(10000);
                } catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });

        当我们点击第一个Button十秒后,Demo APP出现了Crash,APP直接崩掉:
在这里插入图片描述
        异常堆栈信息如下:

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@ba9eb53 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:94)
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:158)
at android.app.ActivityThread.main(ActivityThread.java:6175)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:893)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)

        当我们点击第二个Button显示Toast时,此时是通过使用ToastUtil来显示Toast,ToastUtil是对Toast管理的工具类,内部已经根据第二节分析的解决方法进行了一层封装:

btnFixed.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ToastUtil.showToast(MainActivity.this,"I am fixed Toast");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });

        当我们点击第二个Button十秒后,Demo APP正常运行,而且捕获住了异常:
在这里插入图片描述
在这里插入图片描述

四、ToastUtil

        本文所提交的解决方法已封装成ToastUtil中并提交至GitHub上,具体ToastUtil的实现以及Demo可以参见GitHub项目https://github.com/oukanggui/ToastUtil

五、感谢

        感谢QQ音乐技术团队系列文章的分析,对我有了很大的帮助,对Toast处理有兴趣的同学可以阅读如下两篇QQ音乐技术团队对Toast分析的文章
[Android] Toast问题深度剖析(一)
[Android] Toast问题深度剖析(二)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值