弹一个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的弹出流程,
- 当显示一个Toast时,会通过NotificationManagerService生成一个token
- 调用handleShow的方法,去通过WindowManagerService添加一个窗口
- 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 。