Toast BadTokenException原因及修复方案

弹一个Toast,居然也能够产生crash,你可能难以置信,但的确如此(BadTokenException)
报错堆栈信息如下:

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@bf0c2d7 is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:797)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:351)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
    at android.widget.Toast$TN.handleShow(Toast.java:465)
    at android.widget.Toast$TN$2.handleMessage(Toast.java:347)
    at android.os.Handler.dispatchMessage(Handler.java:110)
    at android.os.Looper.loop(Looper.java:203)
    at android.app.ActivityThread.main(ActivityThread.java:6337)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1084)

从报错信息基本可以看出,Toast内部在执行handleShow的时候,发现了Token存在异常。起初看到这个Crash,也觉得很奇怪,App内部使用的Toast弹出的方式是统一的方法,不至于有的地方存在问题,有的地方没有。后来仔细分析之下,发现在Android 7.1版本以下的机型存在该问题。

先说明一下Toast的弹出流程,

  1. 当显示一个Toast时,会通过NotificationManagerService生成一个token
  2. 调用handleShow的方法,去通过WindowManagerService添加一个窗口
  3. WindowManagerService检查这个token,如果token有效会正常弹出,如果无效,则会抛出对应的异常。
    那这里为什么token会无效了呢?在查看了Android7.1的源代码之后,我们发现Toast在调用显示之后,会去调用一个超时取消的方法来取消这个Toast的展示,然而这里问题就出现了,如果调用了显示方法之后,Toast却因为主线程一直阻塞,没有得到实际显示,这个时候就会导致超时取消显示的方法,可能执行在显示之前。 这里的token就失效了,等到执行到toast的显示的时候,就会抛出crash.
private void scheduleTimeoutLocked(ToastRecord r)
{
   mHandler.removeCallbacksAndMessages(r);
   Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
   long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
   mHandler.sendMessageDelayed(m, delay);
}

本来想着没有弹出来就算了,至少不能出现crash。在添加了cry-catch捕获异常之后,发现依旧会崩溃。你查看代码就会发现这个,异常是直接添加到了消息队列中,当消息队列执行的时候就会发生。
怎么解决这个问题呢?
在分析了相关的源码之后,采用如下的方式:

//替换toast对象内部的mTN对象的mHandler对象,自己来处理错误。具体思路可以参考Android 8.0的源码
fun injectToastProxyTN(toast: Toast) {
    val currentSdk = Build.VERSION.SDK_INT
    if (currentSdk >= Build.VERSION_CODES.LOLLIPOP && currentSdk <= Build.VERSION_CODES.N_MR1) {
        try {
            val tnField = toast.javaClass.getDeclaredField("mTN")
            tnField.isAccessible = true
            val tnObj = tnField.get(toast)
            tnObj ?: return
            val handlerField = tnObj.javaClass.getDeclaredField("mHandler")
            handlerField.isAccessible = true
            handlerField.set(tnObj, ToastProxyTNHandler(tnObj))
        } catch (ex: Exception) {
            Logs.e("inject toast proxy tn failed, the error = $ex")
        }
    }
}


class ToastProxyTNHandler(private val tnObj: Any) : Handler() {

    private var handleShowMethod: Method? = null
    private var handleHideMethod: Method? = null

    init {
        try {
            handleShowMethod = tnObj.javaClass.getDeclaredMethod("handleShow", IBinder::class.java)
            handleShowMethod?.isAccessible = true
            handleHideMethod = tnObj.javaClass.getDeclaredMethod("handleHide")
            handleHideMethod?.isAccessible = true
        } catch (ex: Exception) {

        }
    }

    override fun handleMessage(msg: Message) {
        when (msg.what) {
            0 -> {
                // show
                try {
                    handleShowMethod?.invoke(tnObj, msg.obj)
                } catch (ex: WindowManager.BadTokenException) {

                } catch (ex: IllegalAccessException) {

                } catch (ex: InvocationTargetException) {

                }
            }

            1 -> {
                //hide
                try {
                    handleHideMethod?.invoke(tnObj)
                } catch (ex: IllegalAccessException) {

                } catch (ex: InvocationTargetException) {

                }
            }

            2 -> {
                //cancel
                try {
                    handleHideMethod?.invoke(tnObj)
                } catch (ex: IllegalAccessException) {

                } catch (ex: InvocationTargetException) {

                }
            }
        }
    }
}

对此,我们便修复了Android7.1以下版本Toast抛出BadTokenException的问题。当然该问题也有其他的解决方式,比如github上的ToastCompat

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值