一、問題:
在公司的crash平台上發現了這個crash,機型分布很雜亂,但是都分布在Android25上,量也挺大的,1w的DAU有40個crash,算TopCrash了。其實之前這個crash就一直存在了,但是這次突然crash量大增,沒辦法就分給我解決啦。先看CrashLog:
android.view.WindowManager$BadTokenException: Unable to add window -- token [email protected] is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:683)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
at android.widget.Toast$TN.handleShow(Toast.java:502)
at android.widget.Toast$TN$2.handleMessage(Toast.java:381)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6211)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:793)
初看log,毫無解決頭緒,不知道是哪裡出來的,在Google和Baidu上一番搜尋,終于讓我找到了不少答案。
二、定位:
參考了 http://www.cnblogs.com/qcloud1001/p/8421356.html中的内容,我這裡再簡單的捋一捋,篇幅所限,貼出來的代碼有删減。
1.Toast調用show時,會将一個TN對象交給NMS(NotificationManagerService),作為之後NMS和UI互動的通信管道。
public void show() {
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
2.NMS為toast在WMS(WindowManagerService)中注冊token,再通過TN對象顯示toast,并且向自己postDelay一個隐藏toast的消息,需要注意的是,NMS用的是NMS所線上程的Handler,與後面所說的TN的Handler不是同一個。
public void enqueueToast(String pkg, ITransientNotification callback, int duration{
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
showNextToastLocked();
}
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
try {
// 這裡的callback其實是上個方法傳入的TN對象
record.callback.show(record.token);
// 利用Handler.postDelay了一個隐藏Toast的消息,Delay的值就是Toast的Duration
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
}
}
3.隐藏toast的消息到達(即逾時)之後,NMS會先通過TN對象隐藏toast,,然後将toast在WMS中注冊的token移除。
private void handleTimeout(ToastRecord record){
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
// 隐藏Toast
record.callback.hide();
} catch (RemoteException e) {
}
// 移除Token
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, true,
DEFAULT_DISPLAY);
}
4.我們不關心NMS調用TN這種IPC是怎麼實作的。在TN中,是通過postMessage的方式實作顯示或隐藏toast的效果,而且所用到的Handler與Toast建立線程的Looper關聯,當消息隊列任務較多或者主線程卡死時,消息就會被阻塞。
public void show(IBinder windowToken) {
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
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();
break;
}
case CANCEL: {
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
再接着看handleShow:
public void handleShow(IBinder windowToken) {
if (mView != mNextView) {
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
// Important1
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// Important2
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
這裡有兩個關鍵點:Important1是我們解決問題的關鍵,這個mView就是Toast對應的視圖,而且可以通過getView方式取到;Important2在Android26開始加了try-catch塊,是以隻在Android25才表現出crash。
5.若TN顯示toast的消息在NMS移除了token之後才到達,那麼在TN調用WindowManager.addView的時候就會引發BadTokenException的異常,具體邏輯見WindowManagerImpl。
畫了一個簡單的圖

三、解決
通過上述分析,暫時有三個思路:
第一種是判斷Toast是否顯示然後再決定要不要移除token,如果改NotificationManagerService的方法不太可能,而且用來顯示Toast的TN類沒有判斷Toast是否顯示的方法;
第二種是替換TN類,因為是TN類調用的addView,但是TN類是内部類不能繼承,如果要自己重新寫一個比較麻煩;
第三種就是替換Context了。根據Important1處的代碼,當調用Context的getSystemService方法時傳回一個在addView時進行了try-catch的WindowManager,防止crash。
替換Context是成本最低的,并且在源碼中追蹤之後,getApplicationContext隻在此處有作用,影響也很小。
解決方案:
參考https://github.com/drakeet/ToastCompat,利用反射替換Toast中View的Context