Android PopupWindow Dialog 關于 is your activity running 崩潰詳解

Android PopupWindow Dialog 關于 is your activity running 崩潰詳解

[TOC]

起因

對于 PopupWindow Dialog 需要 Activity 作為容器,并于其生命周期聯系在一起.在Activity 還沒有初始化完成時,此時我們調用 PopupWindow Dialogshow()方法就會拋出異常:

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);
}

如果頁面結束時 PopupWindow Dialog 沒有dismiss(),那么會出現內存泄漏,日志如下:

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執行onDestroy()時 dismiss 掉 PopupWindow Dialog即可;

if (mPopupWindow!=null && mPopupWindow.isShowing()){
            mPopupWindow.dismiss();
  }

源碼

需要涉及的類:

  • ViewManager : WindowManager 的父類.
  • WindowManager 及其實現類 WindowManagerImpl(@hide): 接口類與實現類,對用戶開放.
  • ViewRootImpl(@hide) : WindowManager 的 View 的操作實現類.
  • WindowManagerGlobal(@hide) :WindowManagerImpl 類功能的執行者.

1. 在調用 Show() 方法時
注:以下情況都是對PopupWindow Dialog 適用的,現在不再指明PopupWindow Dialog,下面以PopupWindow為例一步步說明.
直接展示源碼可能更容易說明問題,注釋是關鍵點,以下源碼展示以方法執行順序進行.

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;
        
        //以上都是準備 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();

        //調用WindowManager.addView()方法
        mWindowManager.addView(decorView, p);

        if (mEnterTransition != null) {
            decorView.requestEnterTransition(mEnterTransition);
        }
    }

以上都是在PopupWindow,在調用WindowManager.addView()后進入WindowManager類,實際是其實現類WindowManagerImpl.

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        //調用了 WindowManagerGlobal.addView()方法.
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

重點在這里 WindowManagerGlobal.addView()

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        // ...
        // 檢查一些狀態
        // ViewRootImpl root : 添加 View 最后一步由此 View 操作類的 setView() 完成.
        ViewRootImpl root;
        View panelParentView = null;

            //...
            //準備需要的參數
            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;
        }
    }

最后進入 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:
                        //這里集中處理異常情況
                        //異常實際拋出的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);
                }
                //....省略代碼
            }
        }
     }

從上面的代碼中可以看出,判斷異常類型的是一個int值res,現在看看res.

try {
     mOrigWindowType = mWindowAttributes.type;
     mAttachInfo.mRecomputeGlobalAttributes = true;
     collectViewAttributes();
     // mWindowSession 的類型是 IWindowSession , mWindow 的類型是 IWindow.Stub .這行代碼就是利用AIDL進行IPC, 實際被調用的是Session.addToDisplay()方法.
     res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
           getHostVisibility(), mDisplay.getDisplayId(),
           mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
           mAttachInfo.mOutsets, mInputChannel);
                } catch {//...}

進一步調用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,繼續看 WmS 的 addWindow() 方法.
這是最核心的類,關于所有的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];
        
        // 關于權限的檢查,如果有使用 WindowManager 實現懸浮窗效果的對懸浮窗,關于 SYSTEM_ALERT_WINDOW 的申請問題肯定糾結過.
        // 我在一篇文章說對于SDK 19以上,使用 WindowManager.LayoutParams.TYPE_TOAST,SDK 19 以下使用 WindowManager.LayoutParams.TYPE_PHONE
        // 原因是:1.type為"TYPE_TOAST"在sdk19之前不接收事件,之后可以.
        //       2.type為"TYPE_PHONE"需要"SYSTEM_ALERT_WINDOW"權限.在sdk19之前不可以直接申明使用,之后不能直接申明使用.
        // 想知道為什么會這樣,可以看看 mPolicy.checkAddPermission(attrs, appOp); 答案在這里面.(這里就不看了=.=)
        int res = mPolicy.checkAddPermission(attrs, appOp);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;//如果權限不滿足就不用繼續了.
        }

        boolean reportNewConfig = false;
        WindowState attachedWindow = null;
        long origId;
        final int type = attrs.type;

        //...
        //check something...

            boolean addToken = false;
            
            // mTokenMap 存儲 WindowToken;
            // 這里是取,后面在會執行 mTokenMap.put()方法,這樣 token 就不為null了.
            // Activity 的addWindow時會傳入不為 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調用 WindowManager.addView(),token就會被 put 到 map 中.
                mTokenMap.put(attrs.token, token);
            }
            win.attach();
            mWindowMap.put(client.asBinder(), win);
            //...省略

        return res;
    }

現在應該知道了在哪里拋出異常了,最后還有一點就是關于疑問的: 那 Activity 什么時候 才算 is running ?

Activity 什么時候 才算 is running ?

現在看看 ActivityThread類,這是 Activity 的管理類,分發Activity的生命周期等.
在 ActivityThread 的 handleResumeActivity() 方法中,有如下代碼:

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        //...
        // performResumeActivity() 方法最后會調用 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;
                    //這里調用了 WindowManager 的 addView() 方法,最終會調用 WindowManagerService 的 addWindow() 方法.然后就是之前看到的源碼內容了.其他的情況也類似這個流程,但是還是有很多細微區別,比如 WindowManager.LayoutParams 的 Type 與 Flag 等.
                    wm.addView(decor, l);
                }
            //...
    }

在 Activity 的生命周期 onResume 執行后不久, Activity 的 token 隨著 wm.addView(decor, l); 后就被 put 到 map 中,其后調用 PopupWindow 與 Dialog 的 Show() 方法后就不會出現 "is your activity running ?"這種異常了.由于token在 performResumeActivity() 后(從代碼中可以看出,是在同一方法體中執行完兩個操作),所以有人在 Activity 的 onResume() 方法中這樣寫道:

    @Override
    protected void onResume() {
        super.onResume();
        mListview.postDelayed(new Runnable() {
            @Override
            public void run() {
                mPopupWindow.show(anchor);
            }
        },100);
    }

這種寫法是不可取的.

在實際工作過程中,可能需要在界面展示后2s展示一個Dialog 或者 PopupWindow ,此時如果用戶在2s內退出 Activity ,那么Runnable執行時無法使用一個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社區整理一篇關于 Window 的文章總結的很不錯,對于 Window 不是很了解的可以點此了解下. 我之前整理過PopupWindow 的實現過程,大概有一年了吧,現在竟然忘的差不多了,額....

剩余就是 Activity.isFinishing() 方法的具體調用與實現過程了. 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過程中.有空看看具體過程.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容