前言
最近公司項(xiàng)目最近出現(xiàn)了一個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也需要一個窗口。我們一步來看下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對象,我們看看構(gòu)造方法做了些什么液南。
public Toast(Context context) {
this(context, null);
}
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
// 創(chuàng)建TN對象
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對象創(chuàng)建了TN對象壳猜,并提供了上下文。
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對象傳遞給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ì)的長度最大50條朱嘴,不過在Api29中被改為了25條。由代碼2我們可知使用WindowManager將構(gòu)造的Toast添加到了當(dāng)前的Window中陨瘩,被標(biāo)記Type類型是TypeToast腕够。代碼3處如果當(dāng)先隊(duì)列中沒有元素级乍,則說明直接顯示即可,說明showNextToastLocked()這個方法就是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這個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ù)里资柔,至于這個Token的作用我們后面說焙贷,代碼3調(diào)用WindowManager去添加視圖, 其實(shí)問題也就在這里產(chǎn)生的,當(dāng)token過期失效的時候贿堰,會拋出BadToken異常問題辙芍。熟悉View的繪制流程的話,我們知道WindowManager是個接口羹与,實(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){
// 太長了 省略一堆代碼...
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,他是一個Binder對象,真正的實(shí)現(xiàn)類是Session腾誉,所以Toast在創(chuàng)建過程中也會創(chuàng)建一個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對象,并調(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)持淄薄楞黄!hold 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對象鬼廓,以及標(biāo)記好的Type也就是TYPE_TOKEN。所以我們在代碼3處可以知道當(dāng)token==null的時候致盟,會進(jìn)行異常驗(yàn)證碎税,出現(xiàn)BadToken問題尤慰,所以我們只要找到使之Token失效的原因就可以了。根據(jù)模擬復(fù)現(xiàn)的代碼雷蹂,我們可知調(diào)用了show()方法我們已經(jīng)跨進(jìn)程通訊通知NMS我們要顯示一個吐司伟端,NMS準(zhǔn)備好后再通過跨進(jìn)程通信回調(diào)通知TN, TN在使用Handler發(fā)送信息通知當(dāng)前線程,開始調(diào)用handleShow方法匪煌,并攜帶一個windowToken责蝠。這時候我們調(diào)用了Thread.Sleep()方法,休眠了主線程萎庭,導(dǎo)致Handler阻塞霜医,通知延遲,Sleep()時間一過去驳规,這是又立即通知TN#handleShow方法肴敛,可是這回由于Toast的顯示時間已經(jīng)過去,NMS#scheduleDurationReachedLocked(record);這個方法還在執(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ì)算時間
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)
{
// 到時間了
case MESSAGE_DURATION_REACHED:
handleDurationReached((ToastRecord) msg.obj);
break;
args.recycle();
break;
}
時間到了以后巩搏,cancelToastLocked(index);調(diào)用取消Toast昨登,并將Token置空。這時Toast中的Handler才收到handleShow()贯底,告知WMS創(chuàng)建Window丰辣,但Token已經(jīng)失效所以導(dǎo)致BadToken異常。