上篇文章说道,Android 7.1 Toast 引入了一个系统bug——BadTokenException。Google在android 8.0及时进行了补救。Toast源码也发生了相应变化。
android 8.0开始,Toast的TN对象的show和hide方法,均改为通过TN对象的handler发送Message消息,然后在handleMessage方法里处理。
1.show和hide方法
private static class TN extends ITransientNotification.Stub {
...
@Override
public void show(IBinder windowToken) {
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
@Override
public void hide() {
mHandler.obtainMessage(HIDE).sendToTarget();
}
...
}
2.handleMessage方法里处理SHOW和HIDE消息
private static class TN extends ITransientNotification.Stub {
...
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
mNextView = null;
break;
}
...
}
}
};
...
}
在8.0之前,Toast的cancel方法是直接调用其TN对象的hide方法,并将其从显示队列里去除。
public class Toast{
...
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
...
}
8.0开始,TN定义了cancel方法,Toast直接调用TN对象的cancel方法。
1.定义cancel方法
private static class TN extends ITransientNotification.Stub {
...
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
...
}
2.在handler的handleMessage里处理CANCEL消息
private static class TN extends ITransientNotification.Stub {
...
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
...
case CANCEL: {
handleHide();
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
...
}
}
};
...
}
3.Toast直接调用TN的cancel方法
public class Toast{
...
public void cancel() {
mTN.cancel();
}
...
}
为了解决Android 7.1引入的bug——BadTokenException,google改进了TN的handleShow方法。
首先,BadTokenException是在添加窗口时,windowToken已然失效时产生的。直接在产生BadTokenException的地方增加try-catch块。
private static class TN extends ITransientNotification.Stub {
...
public void handleShow(IBinder windowToken) {
...
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
...
}
...
}
BadTokenException产生的根本原因是:Toast已加入显示队列并为其创建了Token,系统服务ITransientNotification通过TN的Handler发送了执行handleShow方法的Message,但此时主线程正在执行其他任务,handleShow方法在消息队列里迟迟没有得到执行。直到超时后,系统服务ITransientNotification将其从显示队列去除,并将其windowToken设置为无效,再调用TN的hide方法隐藏窗口。在这之后,我们的程序的主线程“腾出空来”,开始处理TN的handler消息,当添加窗口时,token失效,引起BadTokenException。由于8.0开始,展示窗口、超时隐藏窗口、代码调用cancel取消窗口全部走Handler发送消息,在handleMessage里处理的途径。所以在处理显示窗口的消息前,先判断handler里是否有HIDE或CANCEL的消息,有则说明token已然失效,直接返回,不做任何处理。
private static class TN extends ITransientNotification.Stub {
...
public void handleShow(IBinder windowToken) {
...
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
...
}
...
}