Android 7.1.1 系统使用Toast 可能出现的BadTokenException

一. 情况简介

最近在crash 平台上出现了一个BadTokenException 的crash

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@8493c73 is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:826)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:369)
    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:185)
    at android.app.ActivityThread.main(ActivityThread.java:6493)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:916)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:806)

从崩溃栈中可以看出这是一个和Toast 相关的crash。那么这个crash 是怎么产生的呢?

二. 产生原因

1. 稳定复现步骤:

在Toast.show()之后,UI线程做了耗时的操作阻塞了Handler message的处理,如使用Thread.sleep(5000),然后这个崩溃就出现了。

Toast.makeText(TestActivity.this, "hhhhhhh", Toast.LENGTH_SHORT).show();
try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

上面这段代码,在7.1 以下系统不会发生崩溃,但Toast 也不会显示,在7.1 系统则会在5s 后崩溃。这是因为7.1 系统对Toast 新添加了一些改变。

2. Android 7.1系统做的改变

7.1 系统(N MR1),主要改变就是Toast 显示时需要一个Window Token(是一个Binder),当Toast 执行show 方法时会将自己的信息传给NotificationManagerService ,该Service 有个内部类ToastRecord 用来记录Toast 的信息,这时会为Toast 创建一个token,7.1 之前ToastRecord 并没有token 属性


Android 7.1.1 系统使用Toast 可能出现的BadTokenException_第1张图片
image.png

所以,NotificationManagerService 在通知Toast 显示时会将这个token 传过去


Android 7.1.1 系统使用Toast 可能出现的BadTokenException_第2张图片
image.png

上面代码中的callback 的show 方法最后会调用Toast 中的TN 中的 Handler 的handleMessage 方法,然后执行handleShow 方法
Android 7.1.1 系统使用Toast 可能出现的BadTokenException_第3张图片
image.png

然后这个token 被传递给WindowManager.LayoutParams ,WindowManager 执行addView 方法来显示Toast,在addView 方法中如果token 失效就会抛出BadTokenException。通过上面的对比图可以看出7.1 之前Toast 显示不需要token 所以不会crash。

至于token 是怎么失效的,有这样一个流程:

Toast 显示会有一个时长,所以在NotificationManagerService 内也有两个超时时长,跟Toast 的duration 有关,如果Toast 的duration设置的是LONG,则为3.5 s,如果是SHORT,则为2s。NotificationManagerService 会在调用Toast 的show 之后发送一个延时消息,延时时长就是超时时长,这个延时消息就是用来取消Toast 的显示,取消显示会有如下操作:
1)调用Toast 中TN 的hide 方法;
2)将相应index 的ToastRecord 从mToastQueue 队列中移除;
3)将对应的token 移除
4)如果Toast 队列不为空,显示下一个Toast

可以看到有一步操作是移除token。所以,结论来了,当Toast 调用了show 发送了一个Message之后,UI 线程被阻塞住了,超时之后token 失效,这时show 操作接着执行addView 操作时,程序就会发现token 已失效从而抛出异常。

三. 解决方案

这个crash 在8.0(O)是不会存在,因为Google 大佬们也发现了这个问题并用神乎其技的方式进行了修复,没错就是try catch。下面是8.0 源码


Android 7.1.1 系统使用Toast 可能出现的BadTokenException_第4张图片
image.png

那么针对7.1 系统该怎么办呢,可以考虑使用下面两种方式

1. 使用其他提示方式代替Toast
 比如 SnackBar

2. 继承一个Toast 并通过反射的方式将Handler 执行Message 的部分try catch 住
Google 大大都用的try catch 说明这个问题暂时没有更好的解决方案,所以我们也可以用这种方式来做,下面是示例代码

ToastCompat

class ToastCompat(context: Context) : Toast(context) {

    companion object {
        @JvmField val TAG = "ToastCompat"

        @JvmStatic fun makeTextCompat(context: Context, text: CharSequence, duration: Int): Toast{
            val result = ToastCompat(context)

            val inflate = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater

            val view = inflate.inflate(context.resources.getIdentifier("transient_notification", "layout", "android"), null)
            val tv = view.findViewById(android.R.id.message)
            tv.text = text
            result.view = view
            result.duration = duration

            return result
        }

        @JvmStatic private fun setFieldValue(obj: Any, fieldName: String, newValue: Any) {
            val field = getDeclaredField(obj, fieldName)
            field?.let {
                val modifierField = Field::class.java.getDeclaredField("accessFlags")
                modifierField.isAccessible = true
                modifierField.setInt(field, field.modifiers and Modifier.FINAL.inv())

                if (!field.isAccessible) {
                    field.isAccessible = true
                }
                field.set(obj, newValue)
            }

        }

        @JvmStatic private fun getFieldValue(obj: Any, fieldName: String): Any? {
            val field = getDeclaredField(obj, fieldName)
            field?.let {
                if (!field.isAccessible) {
                    field.isAccessible = true
                }
                return field.get(obj)
            }
            return null
        }

        @JvmStatic private fun getDeclaredField(obj: Any, fieldName: String): Field? {
            var superClass = obj.javaClass
            while (superClass != Any::class.java) {
                try {
                    return superClass.getDeclaredField(fieldName)
                } catch (e: NoSuchFieldException) {
                    superClass = superClass.getSuperclass()
                    continue// new add
                }
            }
            return null
        }
    }

    override fun show() {
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
            fixBug()
        }
        super.show()
    }

    private fun fixBug() {
        try {
            val mTN = getFieldValue(this, "mTN")
            mTN?.let {
                val rawHandler = getFieldValue(mTN, "mHandler") as? Handler
                rawHandler?.let {
                    setFieldValue(rawHandler, "mCallback", InternalHandlerCallback(rawHandler))
                }
            }
        } catch (e: Throwable) {
            e.printStackTrace()
        }
    }

    private class InternalHandlerCallback(private val mHandler: Handler) : Handler.Callback {

        override fun handleMessage(msg: Message?): Boolean {
            try {
                mHandler.handleMessage(msg)
            } catch (e: Throwable) {
                e.printStackTrace()
            }
            return true
        }
    }
}

参考:
不同版本代码区别

你可能感兴趣的:(Android 7.1.1 系统使用Toast 可能出现的BadTokenException)