Android PopupWindow Dialog 關(guān)于 is your activity running 崩潰詳解
[TOC]
起因
對于 PopupWindow Dialog
需要 Activity 作為容器,并于其生命周期聯(lián)系在一起.在Activity 還沒有初始化完成時(shí),此時(shí)我們調(diào)用 PopupWindow Dialog
的show()
方法就會(huì)拋出異常:
throw new WindowManager.BadTokenException("Unable to add window -- token " + attrs.token+ " is not valid; is your activity running?");
常見的崩潰日志如下:
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@406a074 is not valid; is your activity running?
at android.view.ViewRoot.setView(ViewRoot.java:530)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:199)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:113)
at android.view.Window$LocalWindowManager.addView(Window.java:424)
at android.app.Dialog.show(Dialog.java:241)
at com.eleybourn.bookcatalogue.dialogs.StandardDialogs.goodreadsAuthAlert(StandardDialogs.java:261)
at com.eleybourn.bookcatalogue.goodreads.GoodreadsUtils$4$1.run(GoodreadsUtils.java:101)
at android.os.Handler.handleCallback(Handler.java:587)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:130)
at android.app.ActivityThread.main(ActivityThread.java:3687)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:507)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:878)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:636)
at dalvik.system.NativeStart.main(Native Method)
解決辦法
if(!Activity.isFinishing()){
mPopupWindow.show(anchor);
}
如果頁面結(jié)束時(shí) PopupWindow Dialog
沒有dismiss()
,那么會(huì)出現(xiàn)內(nèi)存泄漏,日志如下:
SpecialTopicActivity has leaked window android.widget.PopupWindow$PopupDecorView{dfa91cc V.E...... ........ 0,0-185,86} that was originally added here
at android.view.ViewRootImpl.<init>(ViewRootImpl.java:573)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:326)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:109)
at android.widget.PopupWindow.invokePopup(PopupWindow.java:1333)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1156)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1115)
at com.fanwe.customview.PopTipShare.show(PopTipShare.java:56)
at com.fanwe.seller.views.SpecialTopicActivity$4.run(SpecialTopicActivity.java:222)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:7224)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
...
在Activity執(zhí)行onDestroy()
時(shí) dismiss 掉 PopupWindow Dialog
即可;
if (mPopupWindow!=null && mPopupWindow.isShowing()){
mPopupWindow.dismiss();
}
源碼
需要涉及的類:
-
ViewManager
:WindowManager
的父類. -
WindowManager
及其實(shí)現(xiàn)類WindowManagerImpl(@hide)
: 接口類與實(shí)現(xiàn)類,對用戶開放. -
ViewRootImpl(@hide)
: WindowManager 的 View 的操作實(shí)現(xiàn)類. -
WindowManagerGlobal(@hide)
:WindowManagerImpl
類功能的執(zhí)行者.
1. 在調(diào)用 Show() 方法時(shí)
注:以下情況都是對PopupWindow Dialog
適用的,現(xiàn)在不再指明PopupWindow Dialog
,下面以PopupWindow
為例一步步說明.
直接展示源碼可能更容易說明問題,注釋是關(guān)鍵點(diǎn),以下源碼展示以方法執(zhí)行順序進(jìn)行.
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
if (isShowing() || mContentView == null) {
return;
}
TransitionManager.endTransitions(mDecorView);
attachToAnchor(anchor, xoff, yoff, gravity);
mIsShowing = true;
mIsDropdown = true;
final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
preparePopup(p);
final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
p.width, p.height, gravity);
updateAboveAnchor(aboveAnchor);
p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;
//以上都是準(zhǔn)備 WindowManager.LayoutParams p;
invokePopup(p);
}
private void invokePopup(WindowManager.LayoutParams p) {
if (mContext != null) {
p.packageName = mContext.getPackageName();
}
final PopupDecorView decorView = mDecorView;
decorView.setFitsSystemWindows(mLayoutInsetDecor);
setLayoutDirectionFromAnchor();
//調(diào)用WindowManager.addView()方法
mWindowManager.addView(decorView, p);
if (mEnterTransition != null) {
decorView.requestEnterTransition(mEnterTransition);
}
}
以上都是在PopupWindow
,在調(diào)用WindowManager.addView()
后進(jìn)入WindowManager
類,實(shí)際是其實(shí)現(xiàn)類WindowManagerImpl
.
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
//調(diào)用了 WindowManagerGlobal.addView()方法.
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
重點(diǎn)在這里 WindowManagerGlobal.addView()
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// ...
// 檢查一些狀態(tài)
// ViewRootImpl root : 添加 View 最后一步由此 View 操作類的 setView() 完成.
ViewRootImpl root;
View panelParentView = null;
//...
//準(zhǔn)備需要的參數(shù)
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
// do this last because it fires off messages to start doing things
try {
//ViewRootImpl類的 setView() 方法.
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;
}
}
最后進(jìn)入 ViewRootImpl 的 setView() 方法
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
mAttachInfo.mDisplayState = mDisplay.getState();
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
//最終把 View 添加上去,如果異常了就傳null.
mFallbackEventHandler.setView(view);
mWindowAttributes.copyFrom(attrs);
//...省略代碼
if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow);
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
mAdded = false;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
//這里集中處理異常情況
//異常實(shí)際拋出的ADD_BAD_SUBWINDOW_TOKEN
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- another window of type "
+ mWindowAttributes.type + " already exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified display can not be found");
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified window type "
+ mWindowAttributes.type + " is not valid");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
//....省略代碼
}
}
}
從上面的代碼中可以看出,判斷異常類型的是一個(gè)int
值res,現(xiàn)在看看res.
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
// mWindowSession 的類型是 IWindowSession , mWindow 的類型是 IWindow.Stub .這行代碼就是利用AIDL進(jìn)行IPC, 實(shí)際被調(diào)用的是Session.addToDisplay()方法.
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch {//...}
進(jìn)一步調(diào)用Session.java中的addToDisplay方法:
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
mService是WindowManagerService,繼續(xù)看 WmS 的 addWindow() 方法.
這是最核心的類,關(guān)于所有的Android addView 最后都是通過 WmS 的 addWindow()
方法完成添加操作.
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
int[] appOp = new int[1];
// 關(guān)于權(quán)限的檢查,如果有使用 WindowManager 實(shí)現(xiàn)懸浮窗效果的對懸浮窗,關(guān)于 SYSTEM_ALERT_WINDOW 的申請問題肯定糾結(jié)過.
// 我在一篇文章說對于SDK 19以上,使用 WindowManager.LayoutParams.TYPE_TOAST,SDK 19 以下使用 WindowManager.LayoutParams.TYPE_PHONE
// 原因是:1.type為"TYPE_TOAST"在sdk19之前不接收事件,之后可以.
// 2.type為"TYPE_PHONE"需要"SYSTEM_ALERT_WINDOW"權(quán)限.在sdk19之前不可以直接申明使用,之后不能直接申明使用.
// 想知道為什么會(huì)這樣,可以看看 mPolicy.checkAddPermission(attrs, appOp); 答案在這里面.(這里就不看了=.=)
int res = mPolicy.checkAddPermission(attrs, appOp);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;//如果權(quán)限不滿足就不用繼續(xù)了.
}
boolean reportNewConfig = false;
WindowState attachedWindow = null;
long origId;
final int type = attrs.type;
//...
//check something...
boolean addToken = false;
// mTokenMap 存儲(chǔ) WindowToken;
// 這里是取,后面在會(huì)執(zhí)行 mTokenMap.put()方法,這樣 token 就不為null了.
// Activity 的addWindow時(shí)會(huì)傳入不為 null 的 token,然而 PopupWindow 和 Dialog 傳入的是為 null 的token.
//final HashMap<IBinder, WindowToken> mTokenMap = new HashMap<>();
WindowToken token = mTokenMap.get(attrs.token);
AppWindowToken atoken = null;
//...
//check something...
if (addToken) {
//如果activity調(diào)用 WindowManager.addView(),token就會(huì)被 put 到 map 中.
mTokenMap.put(attrs.token, token);
}
win.attach();
mWindowMap.put(client.asBinder(), win);
//...省略
return res;
}
現(xiàn)在應(yīng)該知道了在哪里拋出異常了,最后還有一點(diǎn)就是關(guān)于疑問的: 那 Activity 什么時(shí)候 才算 is running ?
Activity 什么時(shí)候 才算 is running ?
現(xiàn)在看看 ActivityThread類,這是 Activity 的管理類,分發(fā)Activity的生命周期等.
在 ActivityThread 的 handleResumeActivity() 方法中,有如下代碼:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
//...
// performResumeActivity() 方法最后會(huì)調(diào)用 Activity 的 OnResume() 方法.
r = performResumeActivity(token, clearHide, reason);
//....
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient && !a.mWindowAdded) {
a.mWindowAdded = true;
//這里調(diào)用了 WindowManager 的 addView() 方法,最終會(huì)調(diào)用 WindowManagerService 的 addWindow() 方法.然后就是之前看到的源碼內(nèi)容了.其他的情況也類似這個(gè)流程,但是還是有很多細(xì)微區(qū)別,比如 WindowManager.LayoutParams 的 Type 與 Flag 等.
wm.addView(decor, l);
}
//...
}
在 Activity 的生命周期 onResume 執(zhí)行后不久, Activity 的 token 隨著 wm.addView(decor, l);
后就被 put 到 map 中,其后調(diào)用 PopupWindow 與 Dialog 的 Show() 方法后就不會(huì)出現(xiàn) "is your activity running ?"這種異常了.由于token在 performResumeActivity() 后(從代碼中可以看出,是在同一方法體中執(zhí)行完兩個(gè)操作),所以有人在 Activity 的 onResume() 方法中這樣寫道:
@Override
protected void onResume() {
super.onResume();
mListview.postDelayed(new Runnable() {
@Override
public void run() {
mPopupWindow.show(anchor);
}
},100);
}
這種寫法是不可取的.
在實(shí)際工作過程中,可能需要在界面展示后2s展示一個(gè)Dialog 或者 PopupWindow ,此時(shí)如果用戶在2s內(nèi)退出 Activity ,那么Runnable執(zhí)行時(shí)無法使用一個(gè)Finished 的Activity 這里推薦的寫法.
//第一種
mSomeView.postDelayed(new Runnable() {
@Override
public void run() {
if (!Activity.this.isFinishing()){
mPopupWindow.show();
}
}
},1000);
//第二種
mSomeView.post(new Runnable() {
@Override
public void run() {
if (!Activity.this.isFinishing()){
mPopupWindow.show();
}
}
});
Bugly社區(qū)整理一篇關(guān)于 Window 的文章總結(jié)的很不錯(cuò),對于 Window 不是很了解的可以點(diǎn)此了解下. 我之前整理過PopupWindow 的實(shí)現(xiàn)過程,大概有一年了吧,現(xiàn)在竟然忘的差不多了,額....
剩余就是 Activity.isFinishing() 方法的具體調(diào)用與實(shí)現(xiàn)過程了. Google API 的說明是:
Check to see whether this activity is in the process of finishing, either because you called finish on it or someone else has requested that it finished. This is often used in onPause to determine whether the activity is simply pausing or completely finishing.
檢測 Activity 是否在 Process 或者 Finishing過程中.有空看看具體過程.