前言
最近公司項(xiàng)目最近出現(xiàn)了一個(gè)Toast引發(fā)的BadTokenException崩潰,集中在Android5.0 - Android7.1.2版本,經(jīng)過分析解決了,所以現(xiàn)在想記錄一下。
崩潰日志
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@cf6e52d 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:93)
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:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
模擬復(fù)現(xiàn)
// android api25 7.1.1
tv.setOnClickListener {
Toast.makeText(this, testEntity.nameLi,Toast.LENGTH_SHORT).show()
Thread.sleep(3000)
}
源碼復(fù)習(xí)
在Android中,我們知道所有的UI視圖都是依附于Window,因此Toast也需要一個(gè)窗口。我們一步來看下Toast源碼。
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);
// 找到布局文件 并在布局文件中找到要展示的TextView控件并賦值
LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
我們熟知的調(diào)用makeText().show()方法即可將Toast彈窗展示出來,makeText()方法中實(shí)例化了Toast對(duì)象,我們看看構(gòu)造方法做了些什么。
public Toast(Context context) {
this(context, null);
}
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
// 創(chuàng)建TN對(duì)象
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize(com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(com.android.internal.R.integer.config_toastDefaultGravity);
}
通過構(gòu)造函數(shù)我們知道初始化Toast對(duì)象創(chuàng)建了TN對(duì)象,并提供了上下文。
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();
try {
// 添加到Toast顯示隊(duì)列
service.enqueueToast(pkg, tn, mDuration, displayId);
} catch (RemoteException e) {
// Empty
}
}
由方法名可見,Toast的顯示是加入到隊(duì)列中,但是如何加入隊(duì)列中的呢?其實(shí)Toast并非是由自身控制,而是通過AIDL進(jìn)程間通信,將Toast信息傳遞給NMS遠(yuǎn)程通知管理器進(jìn)行統(tǒng)一管理,enqueueToast()方法就是把TN對(duì)象傳遞給NMS并回傳過來用于標(biāo)志Toast顯示狀態(tài)。
NotificationManagerService#enqueueToast()
// 集合隊(duì)列
final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();
...省略部分代碼
synchronized (mToastQueue) {
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback){
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// 是不是系統(tǒng)的Toast
if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
if (r.pkg.equals(pkg)) {
count++;
// 1
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
Binder token = new Binder();
// 2
mWindowManagerInternal.addWindowToken(token,
WindowManager.LayoutParams.TYPE_TOAST);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveIfNeededLocked(callingPid);
}
if (index == 0) {
// 3
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
由1處代碼我們知道Toast排隊(duì)的長(zhǎng)度最大50條,不過在Api29中被改為了25條。由代碼2我們可知使用WindowManager將構(gòu)造的Toast添加到了當(dāng)前的Window中,被標(biāo)記Type類型是TypeToast。代碼3處如果當(dāng)先隊(duì)列中沒有元素,則說明直接顯示即可,說明showNextToastLocked()這個(gè)方法就是NMS通知顯示的Toast的方法。
NotificationManagerService#showNextToastLocked()
ToastRecord(int pid, String pkg, ITransientNotification callback, int duration,
Binder token) {
this.pid = pid;
this.pkg = pkg;
this.callback = callback;
this.duration = duration;
this.token = token;
}
void showNextToastLocked() {
// 1
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
// 2
record.callback.show(record.token);
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
// 省略
}
}
}
代碼1處從集合中拿到index=0的ToastRecord, 2處代碼調(diào)用ITransientNotification#show()方法并傳入token這個(gè)token關(guān)鍵,之后回調(diào)到TN中的show()方法之中了。
TN#show()
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
這里通過Handler轉(zhuǎn)發(fā)到主線程中處理異步信息,我們看收到消息后,怎么處理的
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
// 1
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
// 2
mParams.token = windowToken;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
// 3
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
代碼1獲取到WindowManager,方法2 將Token(Binder)放到參數(shù)里,至于這個(gè)Token的作用我們后面說,代碼3調(diào)用WindowManager去添加視圖, 其實(shí)問題也就在這里產(chǎn)生的,當(dāng)token過期失效的時(shí)候,會(huì)拋出BadToken異常問題。熟悉View的繪制流程的話,我們知道WindowManager是個(gè)接口,實(shí)現(xiàn)類是WindowManagerImpl,最終addView方法是調(diào)用WindowManagerGlobal的addView()方法。
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
// ... 省略代碼
ViewRootImpl root;
View panelParentView = null;
// ... 省略代碼
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
try {
// 1
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
1處代碼已經(jīng)顯現(xiàn)出問題的原因了,我們進(jìn)入ViewRootImpl看下setView()方法;
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView){
// 太長(zhǎng)了 省略一堆代碼...
int res;
// 1
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
}
}
mWindowSession 的類型是IWindowSession,他是一個(gè)Binder對(duì)象,真正的實(shí)現(xiàn)類是Session,所以Toast在創(chuàng)建過程中也會(huì)創(chuàng)建一個(gè)Window,之后就是Window的創(chuàng)建過程,我們一起在屢一下Window的創(chuàng)建過程。
@UnsupportedAppUsage
final IWindowSession mWindowSession;
mWindowSession = WindowManagerGlobal.getWindowSession();
看WindowSession是在WindowManagerGlobal中獲取的,我們跟進(jìn)下:
@UnsupportedAppUsage
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
@UnsupportedAppUsage
InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
// 1
IWindowManager windowManager = getWindowManagerService();
// 3
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
}
@UnsupportedAppUsage
public static IWindowManager getWindowManagerService() {
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
// 2
sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));
try {
if (sWindowManagerService != null) {
ValueAnimator.setDurationScale(
sWindowManagerService.getCurrentAnimatorScale());
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowManagerService;
}
}
代碼1和2處我們看到通過AIDL遠(yuǎn)程調(diào)用到了WindowManagerService對(duì)象,并調(diào)用了openSession()方法。
@Override
public IWindowSession openSession(IWindowSessionCallback callback) {
return new Session(this, callback);
}
由此可知ViewRootImpl#setView()最終調(diào)用了Session類的addToDisplay()
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState) {
// 1
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,
outInsetsState);
}
真是轉(zhuǎn)來轉(zhuǎn)去最終由要回到WindowManagerService#addWindow()真是一波三折??!不過這里使用了門面模式,最終實(shí)現(xiàn)都交給了WMS。堅(jiān)持?。old on !馬上到高潮了。
public int addWindow(Session session, IWindow client, int seq,
LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState) {
// 省略...
// 1
WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
// 2
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
if (token == null) {
// 3
if (type == TYPE_TOAST) {
if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
parentWindow)){
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
}
}
1處獲取到在TN中創(chuàng)建好的WindowManager.LayoutParams中的Token也就是IBinder對(duì)象,以及標(biāo)記好的Type也就是TYPE_TOKEN。所以我們?cè)诖a3處可以知道當(dāng)token==null的時(shí)候,會(huì)進(jìn)行異常驗(yàn)證,出現(xiàn)BadToken問題,所以我們只要找到使之Token失效的原因就可以了。根據(jù)模擬復(fù)現(xiàn)的代碼,我們可知調(diào)用了show()方法我們已經(jīng)跨進(jìn)程通訊通知NMS我們要顯示一個(gè)吐司,NMS準(zhǔn)備好后再通過跨進(jìn)程通信回調(diào)通知TN, TN在使用Handler發(fā)送信息通知當(dāng)前線程,開始調(diào)用handleShow方法,并攜帶一個(gè)windowToken。這時(shí)候我們調(diào)用了Thread.Sleep()方法,休眠了主線程,導(dǎo)致Handler阻塞,通知延遲,Sleep()時(shí)間一過去,這是又立即通知TN#handleShow方法,可是這回由于Toast的顯示時(shí)間已經(jīng)過去,NMS#scheduleDurationReachedLocked(record);這個(gè)方法還在執(zhí)行 不受應(yīng)用進(jìn)程中的線程睡眠的影響。
@GuardedBy("mToastQueue")
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
// 通知TN顯示,并向WMS發(fā)送消息
record.callback.show(record.token);
// 計(jì)算時(shí)間
scheduleDurationReachedLocked(record);
return;
} catch (RemoteException e) {
// 省略...
}
}
}
@GuardedBy("mToastQueue")
private void scheduleDurationReachedLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
int delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
AccessibilityManager.FLAG_CONTENT_TEXT);
// 使用Handler發(fā)送延遲移除視圖(Toast)消息
mHandler.sendMessageDelayed(m, delay);
}
switch (msg.what)
{
// 到時(shí)間了
case MESSAGE_DURATION_REACHED:
handleDurationReached((ToastRecord) msg.obj);
break;
args.recycle();
break;
}
時(shí)間到了以后,cancelToastLocked(index);調(diào)用取消Toast,并將Token置空。這時(shí)Toast中的Handler才收到handleShow(),告知WMS創(chuàng)建Window,但Token已經(jīng)失效所以導(dǎo)致BadToken異常。