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

2019-08-07  本文已影响0人  wind_sky

一. 情况简介

最近在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 属性


image.png

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


image.png
上面代码中的callback 的show 方法最后会调用Toast 中的TN 中的 Handler 的handleMessage 方法,然后执行handleShow 方法
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 源码


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<TextView>(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
        }
    }
}

参考:
不同版本代码区别

上一篇下一篇

猜你喜欢

热点阅读