Toast系列(四):Android 7.1系统Toast BadTokenException解决方案

Toast系列(二):Toast基本工作原理(android 7.1变化)中我们说到,Android7.1系统引入了Toast的一个bug——BadTokenException。本篇我们剖析下原因并给出解决方案。

Android7.1开始,系统服务在将Toast请求加入队列时,为其创建一个Token。

@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration){
   ...
   Binder token = new Binder();
   //为该Toast窗口添加Token,笔者注释
   mWindowManagerInternal.addWindowToken(token,WindowManager.LayoutParams.TYPE_TOAST);
   record = new ToastRecord(callingPid, pkg, callback, duration, token);
   //将该Toast显示请求加入队列,笔者注释
   mToastQueue.add(record);
   ...
}

当Toast超时(duration耗尽)或者主动cancel掉时,系统服务先隐藏掉该Toast窗口,然后将请求从队列移除,并将Token设为失效。

void cancelToastLocked(int index) {
       //从队列中把本次请求取出,笔者注释
       ToastRecord record = mToastQueue.get(index);
        ...
        //调用callback的hide方法隐藏掉窗口,这个callback实际上就是Toast的Tn,笔者注释
        record.callback.hide();
        ...
        //将该Toast请求从队列移除,笔者注释
        ToastRecord lastToast = mToastQueue.remove(index);
        //将该Toast窗口的Token设为无效,笔者注释
        mWindowManagerInternal.removeWindowToken(lastToast.token, true);
        ...
        //如果队列不为空,则说明还有Toast要显示,则继续显示下一个Toast,笔者注释
        if (mToastQueue.size() > 0) {
            showNextToastLocked();
        }
    }

Toast的工作流程是一个基于Binder的IPC(进程)过程,应用程序作为客户端仅仅发起Toast请求和被动接受回调。系统服务负责管理请求队列、Token等,并通过Toast的内部类Tn来实现与应用程序的交互,即通过回调Tn的show/hide方法来显示/隐藏Toast窗口。

应用程序的主线程是一个死循环,不断地从消息队列里取出消息执行。系统服务回调Tn的show方法,实际执行逻辑是发送一个SHOW消息给Tn的Handler,并最终在这个Handler的handleShow方法里执行显示逻辑。

Tn的show方法:

        @Override
        public void show(IBinder windowToken) {
            ...
            mHandler.obtainMessage(0, windowToken).sendToTarget();
        }

Handler的handleShow方法:

        public void handleShow(IBinder windowToken) {
                ...
                //将系统服务创建的Token传递进来,笔者注释
                mParams.token = windowToken;
                ...
                //添加Toast窗口,此时Token已然失效,引发BadTokenExceptin,笔者注释
                mWM.addView(mView, mParams);
                ...
        }

如果系统服务已然调用了Tn的show方法,而恰在此时,应用程序主线程因为某个消息阻塞或者其他原因,迟迟没能执行handleShow方法。直到Toast超时,系统服务将该Toast请求从队列移除,并将Token设为失效。在这之后,应用程序才执行到handleShow方法,而Token已然失效,添加Toast窗口必然发生BadTokenException。

Google在Android8.0修复了这个bug。

首先,直接在产生BadTokenException的地方捕获该异常。

        public void handleShow(IBinder windowToken) {
                ...
                //捕获该异常,笔者注释
                try {
                    mWM.addView(mView, mParams);
                ...
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

另外,在Tn的handler处理SHOW消息之前,判断其消息队列里是否存在HIDE或者CANCEL消息,有则表示Token已然失效,直接返回,什么都不需要做。

       public void handleShow(IBinder windowToken) {
            ...

            //判断是否有超时隐藏或者主动cancel的消息,有则表示系统服务已经将其从
            //显示队列移除,并将Token设为失效,笔者注释
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }

            ...

                try {
                    mWM.addView(mView, mParams);
                    ...
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            
        }

但是,对于已经发布了的Android7.1,我们如何解决这个问题呢?我们可以仿照8.0的补救方法,直接添加try-catch块,捕获它。

注意,不能想当然的像下面这样捕获。

        try {
            toast.show();
        }catch (WindowManager.BadTokenException e){

        }

Toast的show方法仅仅是创建Tn提交给系统服务,请求显示Toast而已。前面说过,BadTokenException发生在handleShow方法内,而handleShow方法是在Tn的Handler的handleMessage方法内被调用的。我们可以新建一个Handler,命名为SafeHandler吧,作为Tn原有Handler的外壳。SafeHandler的handleMessage方法直接转交给原有Handler处理,只是在外层套上try-catch块。最后将SafeHandler注入Tn,取代原有Handler。

SafeHandler源码:

class SafeHandler extends Handler {
    //用来保存Tn原有handler
    private Handler mNestedHandler;

    public SafeHandler(Handler nestedHandler) {
        //构造方法里将Tn原有Handler传入
        mNestedHandler = nestedHandler;
    }

    /**
     * handleMessage是在dispatchMessage里被调用的,所以在这里捕获异常就可以
     * @param msg
     */
    @Override
    public void dispatchMessage(Message msg) {
        try {
            super.dispatchMessage(msg);
        } catch (WindowManager.BadTokenException e) {
        }
    }

    @Override
    public void handleMessage(Message msg) {
        //交由原有Handler处理
        mNestedHandler.handleMessage(msg);
    }

通过反射拿到Tn,将SafeHandler注入。

            Field tnField = Toast.class.getDeclaredField("mTN");
            tnField.setAccessible(true);
            mTn = tnField.get(mToast);

            if (isSdk25()) {
                Field handlerField = mTn.getClass().getDeclaredField("mHandler");
                handlerField.setAccessible(true);
                Handler handlerOfTn = (Handler) handlerField.get(mTn);
                handlerField.set(mTn, new SafeHandler(handlerOfTn));
            }

如此,我们完美解决了Android7.1系统关于Toast的BadTokenException。如果你不想自己处理这个问题,我给大家推荐一下我的开源库SmartShow,其中的SmartToast自动帮你解决BadTokenException以及其他常见的Toast性能问题,具体功能请参看github文档:https://github.com/the-pig-of-jungle/SmartShow

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vincent(朱志强)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值