最近在项目新版本测试中,当在Android 7.x(SDK=24/25)设备上跑Monkey测试APP时,经常报Token失效异常:“android.view.WindowManager$BadTokenException: Unable to add window – token android.os.BinderProxy@ba9eb53 is not valid; is your activity running?”,导致APP出现Crash,直接终止运行:
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@ba9eb53 is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:679)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
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:158)
at android.app.ActivityThread.main(ActivityThread.java:6175)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:893)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
从上面异常堆栈信息可以看出,异常发生在当系统Toast内部类对象TN内部的Handler在收到显示消息Message,进行处理并调用 Toast$TN.handleShow()方法时,Toast$TN.handleShow()方法在不同的Android版本中实现也不一样:
在Android 7.x版本,handleShow()方法实现如下:
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
....
mParams.token = windowToken;
...
mWM.addView(mView, mParams);
...
}
}
在Android 8.0版本,handleShow()方法实现如下:
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
....
mParams.token = windowToken;
...
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
...
}
}
在上面可以看出,Google已经在Android 8.0源码中在调用WindowManagerImpl.addView()前通过使用try-catch捕获WindowManager.BadTokenException异常,修复了该bug,避免APP发生Crash,这也是为什么该异常在Android 7.x设备上频繁出现,而在Android 8.0设备上几乎没有发生的原因。
在Android 7.x设备上,通常情况下,按照正常的流程,是不会出现这种异常。但是由于在某些情况下, 尤其是在跑Monkey测试的时候,Android 进程某个 UI 线程的某个消息阻塞,导致 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息之后,迟迟没有执行,导致超时引起NotificationManager超时检测机制删除WMS 服务中的 Token 记录,很容易导致该异常发生,具体Toast显示流程源码分析以及异常产生原因可以阅读下面QQ音乐技术团队的分析文章[Android] Toast问题深度剖析(一)
正如上面分析,异常发生在当系统Toast内部类对象TN内部的Handler在收到显示消息Message,进行处理并调用 Toast$TN.handleShow()方法时,handleShow()方法是Toast内部类TN的方法,我们无法通过直接继承Toast重写handleShow()方法来捕获该异常,不过通过异常堆栈信息可知,在调用Toast$TN.handleShow()前,会先调用Toast$TN$Hanlder.handleMessage(),而调用Toast$TN$Hanlder.handleMessage()前,一定会先调用Handler.dispatchMessage() 方法,我们可以创建一个安全的Handler装饰器,通过重写Handler.dispatchMessage() 方法捕获抛出的异常即可,装饰器Handler实现代码如下:
/**
* Safe outside Handler class which just warps the system origin handler object in the Toast.class
*/
private static class SafelyHandlerWarpper extends Handler {
private Handler originHandler;
public SafelyHandlerWarpper(Handler originHandler) {
this.originHandler = originHandler;
}
@Override
public void dispatchMessage(Message msg) {
// 在此处使用try-catch捕获BadTokenException,当内部Hanlder发生异常,外部SafelyHandlerWarpper可以捕获,
// 防止应用Crash
try {
super.dispatchMessage(msg);
} catch (Exception e) {
Log.e(TAG, "Catch system toast exception:" + e);
}
}
@Override
public void handleMessage(Message msg) {
// 需要委托给原Handler执行
if (originHandler != null) {
originHandler.handleMessage(msg);
}
}
}
然后,我们需要使用定义的SafeHandlerWarpper对象去包装 Toast$TN$Hanlder, 然后通过反射去替换 Toast$TN$Hanlder对象,具体请见如下hookToast()方法:
private static final String FIELD_NAME_TN = "mTN";
private static final String FIELD_NAME_HANDLER = "mHandler";
/**
* Hook Toast,修复在7.x手机上跑monkey的时候,Toast低概率出现BadTokenException的异常
*
* @param toast
*/
private static void hookToast(Toast toast) {
if (!isNeedHook()) {
return;
}
try {
if (!sIsHookFieldInit) {
sField_TN = Toast.class.getDeclaredField(FIELD_NAME_TN);
sField_TN.setAccessible(true);
sField_TN_Handler = sField_TN.getType().getDeclaredField(FIELD_NAME_HANDLER);
sField_TN_Handler.setAccessible(true);
sIsHookFieldInit = true;
}
Object tn = sField_TN.get(toast);
Handler originHandler = (Handler) sField_TN_Handler.get(tn);
sField_TN_Handler.set(tn, new SafelyHandlerWarpper(originHandler));
} catch (Exception e) {
Log.e(TAG, "Hook toast exception=" + e);
}
}
我们仅需要选择在Android 7.x设备(SDK版本为24或25)上使用自定义Handler装饰器SafelyHandlerWarpper去hook系统的Toast$TN$Hanlder对象,如果项目在其他Android版本上也出现该异常,可以根据自己项目需要去添加即可,具体请见如下isNeedHook()方法:
/**
* Check if Toast need hook,only hook the device 7.x(api = 24/25)
*
* @return true for need hook to fit system bug,false for don't need hook
*/
private static boolean isNeedHook() {
return Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1 ||
Build.VERSION.SDK_INT == Build.VERSION_CODES.N;
}
最后,在我们调用Toast.show()准备显示Toast前,调用hookToast()方法即可:
if (mToast == null) {
mToast = Toast.makeText(context, text, duration);
} else {
mToast.setText(text);
mToast.setDuration(duration);
}
hookToast(mToast);
mToast.show();
}
正如上面所说,当UI线程阻塞时,很容易导致该问题产生,我们可以通过在调用Toast.show()方法后,在主线程中调用Thread.sleep()阻塞主线程,导致WMS Token超时失效,就可以在Android 7.x设备上复现该Exception,如下代码所示
Toast.makeText(MainActivity.this,"I am origin Toast without fix",Toast.LENGTH_SHORT).show();
try {
Thread.sleep(10000);
} catch (InterruptedException e){
e.printStackTrace();
}
下面通过Demo来复现以及验证,本例子中,Demo UI设计如下:
当点击第一个Button时,我们直接使用系统的Toast来显示Toast:
btnUnfixed.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,"I am origin Toast without fix",Toast.LENGTH_SHORT).show();
try {
// just sleep and block the main thread which will reappear the BadTokenException
Thread.sleep(10000);
} catch (InterruptedException e){
e.printStackTrace();
}
}
});
当我们点击第一个Button十秒后,Demo APP出现了Crash,APP直接崩掉:
异常堆栈信息如下:
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@ba9eb53 is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:679)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
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:158)
at android.app.ActivityThread.main(ActivityThread.java:6175)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:893)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
当我们点击第二个Button显示Toast时,此时是通过使用ToastUtil来显示Toast,ToastUtil是对Toast管理的工具类,内部已经根据第二节分析的解决方法进行了一层封装:
btnFixed.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ToastUtil.showToast(MainActivity.this,"I am fixed Toast");
try {
Thread.sleep(10000);
} catch (InterruptedException e){
e.printStackTrace();
}
}
});
当我们点击第二个Button十秒后,Demo APP正常运行,而且捕获住了异常:
本文所提交的解决方法已封装成ToastUtil中并提交至GitHub上,具体ToastUtil的实现以及Demo可以参见GitHub项目https://github.com/oukanggui/ToastUtil
感谢QQ音乐技术团队系列文章的分析,对我有了很大的帮助,对Toast处理有兴趣的同学可以阅读如下两篇QQ音乐技术团队对Toast分析的文章
[Android] Toast问题深度剖析(一)
[Android] Toast问题深度剖析(二)