Android PopupWindow Dialog 關(guān)于 is your activity running 崩潰詳解

Android PopupWindow Dialog 關(guān)于 is your activity running 崩潰詳解

[TOC]

起因

對于 PopupWindow Dialog 需要 Activity 作為容器,并于其生命周期聯(lián)系在一起.在Activity 還沒有初始化完成時(shí),此時(shí)我們調(diào)用 PopupWindow Dialogshow()方法就會(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過程中.有空看看具體過程.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市笛丙,隨后出現(xiàn)的幾起案子修陡,更是在濱河造成了極大的恐慌枕赵,老刑警劉巖戏溺,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驯遇,死亡現(xiàn)場離奇詭異焙畔,居然都是意外死亡谱仪,警方通過查閱死者的電腦和手機(jī)玻熙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疯攒,“玉大人嗦随,你說我怎么就攤上這事【闯撸” “怎么了枚尼?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長砂吞。 經(jīng)常有香客問我署恍,道長,這世上最難降的妖魔是什么蜻直? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任盯质,我火速辦了婚禮,結(jié)果婚禮上概而,老公的妹妹穿的比我還像新娘呼巷。我一直安慰自己,他們只是感情好赎瑰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布王悍。 她就那樣靜靜地躺著,像睡著了一般餐曼。 火紅的嫁衣襯著肌膚如雪压储。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天晋辆,我揣著相機(jī)與錄音渠脉,去河邊找鬼宇整。 笑死瓶佳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鳞青。 我是一名探鬼主播霸饲,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼为朋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了厚脉?” 一聲冷哼從身側(cè)響起习寸,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎傻工,沒想到半個(gè)月后霞溪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡中捆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年鸯匹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泄伪。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡殴蓬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蟋滴,到底是詐尸還是另有隱情染厅,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布津函,位于F島的核電站肖粮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏尔苦。R本人自食惡果不足惜尿赚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蕉堰。 院中可真熱鬧凌净,春花似錦、人聲如沸屋讶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽皿渗。三九已至斩芭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乐疆,已是汗流浹背划乖。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挤土,地道東北人琴庵。 一個(gè)月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親迷殿。 傳聞我的和親對象是個(gè)殘疾皇子儿礼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容