踩坑之路:DialogFragment#show()出現(xiàn)crash

DialogFragment這個(gè)控件作為一個(gè)Android開發(fā)者來說,應(yīng)該都是再熟悉不過的了。不過在showDialogFragment發(fā)的時(shí)候經(jīng)常會(huì)碰到下面這個(gè)crash:

      java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
        at androidx.fragment.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:2080)
        at androidx.fragment.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:2106)
        at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:683)
        at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:637)
        at androidx.fragment.app.DialogFragment.show(DialogFragment.java:144)
        at com.netease.demo.MainActivity$1.run(MainActivity.java:23)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

對(duì)于上面這個(gè)問題,首先肯定是得去查看源碼為什么會(huì)出現(xiàn)這個(gè)錯(cuò)誤
一般來說使用DialogFragment基本上就是下面這種寫法:

                DialogFragment dialogFragment = new DialogFragment();
                dialogFragment.show(getSupportFragmentManager(),"");
  1. 獲取宿主即當(dāng)前Activity所持有的FragmentManager,是一個(gè)FragmentManagerImpl對(duì)象
  2. 調(diào)用dialogFragment的show方法
  3. 通過FragmentManagerImpl調(diào)用beginTransaction方法new了一個(gè)BackStackRecord對(duì)象
  4. 通過BackStackRecord將Fragment添加到布局中的控件中
  5. 通過事務(wù)提交這次操作。

crash就出現(xiàn)在了第五步中,我們來看下第四步的邏輯:

    @Override
    public int commit() {
        return commitInternal(false);
    }
   int commitInternal(boolean allowStateLoss) {
        if (mCommitted) throw new IllegalStateException("commit already called");
        if (FragmentManagerImpl.DEBUG) {
            Log.v(TAG, "Commit: " + this);
            LogWriter logw = new LogWriter(TAG);
            PrintWriter pw = new PrintWriter(logw);
            dump("  ", null, pw, null);
            pw.close();
        }
        mCommitted = true;
        if (mAddToBackStack) {
            mIndex = mManager.allocBackStackIndex(this);
        } else {
            mIndex = -1;
        }
        mManager.enqueueAction(this, allowStateLoss);
        return mIndex;
    }

邏輯非常簡(jiǎn)單,先判斷當(dāng)前Fragment添加的事務(wù)是否已經(jīng)被提交過一次(防止復(fù)提交出現(xiàn)頁(yè)面錯(cuò)亂的check)。然后直接調(diào)用FragmentManager的enqueueAction方法:

    public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
        if (!allowStateLoss) {
            checkStateLoss();
        }
        synchronized (this) {
            if (mDestroyed || mHost == null) {
                if (allowStateLoss) {
                    // This FragmentManager isn't attached, so drop the entire transaction.
                    return;
                }
                throw new IllegalStateException("Activity has been destroyed");
            }
            if (mPendingActions == null) {
                mPendingActions = new ArrayList<>();
            }
            mPendingActions.add(action);
            scheduleCommit();
        }
    }
    private void checkStateLoss() {
        if (isStateSaved()) {
            throw new IllegalStateException(
                    "Can not perform this action after onSaveInstanceState");
        }
        if (mNoTransactionsBecause != null) {
            throw new IllegalStateException(
                    "Can not perform this action inside of " + mNoTransactionsBecause);
        }
    }

關(guān)鍵就處在了 checkStateLoss();這里,isStateSaved是true的時(shí)候就會(huì)拋出這個(gè)異常,那么什么時(shí)候拋出呢?

    @Override
    public boolean isStateSaved() {
        // See saveAllState() for the explanation of this.  We do this for
        // all platform versions, to keep our behavior more consistent between
        // them.
        return mStateSaved || mStopped;
    }

是在當(dāng)前宿主頁(yè)面onStop的時(shí)候會(huì)是true(回到home,或者是被其他窗口遮擋),這個(gè)時(shí)候如果再去執(zhí)行commit的話那就直接crash了。


這個(gè)時(shí)候就需要想辦法解決這個(gè)問題了,所以以下列出四種方法:

  1. 在每個(gè)DialogFragment的show方法調(diào)用之前,都判斷當(dāng)前宿主Activity是否已經(jīng)被其他窗口覆蓋或者進(jìn)入home了??梢詫懸粋€(gè)boolean變量,在onSaveInstance執(zhí)行的時(shí)候就置為true,然后根據(jù)這個(gè)變量判斷。
    不過這個(gè)方案浸入性太強(qiáng),需要修改的地方太多了。這不是個(gè)好方案

  2. 既然commit直接crash,那么怎么做才能阻止crash呢。我們?cè)倏聪耬nqueueAction方法:

        if (!allowStateLoss) {
            checkStateLoss();
        }

只有在allowStateLoss為false的時(shí)候才需要去做checkStateLoss操作,所以我們只要設(shè)置alloStateLoss為true即可。不過我們沒法修改DialogFragment的show方法,所以我們可以在BaseDialogFragment里面重寫show方法

    public void show(FragmentManager manager, String tag) {
        mDismissed = false;
        mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        //將ft.commit改成ft.commitAllowingStateLoss()。
        ft.commit();
    }

將ft.commit改成ft.commitAllowingStateLoss()。這樣就解決了浸入性強(qiáng)的問題,但是這里又有一個(gè)問題了,因?yàn)閙Dismissed和mShownByMe是默認(rèn)訪問權(quán)限的變量,我們自己的APP無法直接獲取這個(gè)參數(shù),所以也就沒法直接修改這個(gè)參數(shù),因此這兩個(gè)參數(shù)未被修改可能會(huì)引發(fā)其他的問題。所以這個(gè)方案也不行。

  1. 與方案二類似,既然我們無法獲取這兩個(gè)參數(shù),那我們干脆直接將DialogFragment的源碼拷貝出來,自己寫一個(gè)DialogFragment,這樣就可以修改show方法了。這種方案雖然沒有什么弊端,但是個(gè)人還是不太贊成這個(gè)方案的。

  2. 接下來就是我比較推薦的方案了,我們重新捋下邏輯:
    為什么DialogFragment會(huì)crash? 是因?yàn)槲覀冊(cè)赾heckStateLoss();發(fā)現(xiàn)當(dāng)前宿主Activity已經(jīng)不在屏幕上顯示了,所以才會(huì)拋出這個(gè)異常,那么我們能否在show的時(shí)候判斷呢?自然是可以的,我們的第一種方案就是,不過第一種方案是在show外面判斷,接下來我要介紹的方案是重寫show方法:

    /**
     * 檢查當(dāng)前頁(yè)面是否處于活躍狀態(tài)
     * @param manager 當(dāng)前Activity對(duì)應(yīng)的fragment管理者
     * @return
     */
    protected boolean checkActivityIsActive(FragmentManager manager) {
        if(manager.isStateSaved()){
            return false;
        }
        return true;
    }

    @Override
    public void show(FragmentManager manager, String tag) {
        if(!checkActivityIsActive(manager)){
            return;
        }
        super.show(manager, tag);
    }

既然checkStateLoss是在FragmentManager里面判斷的,所以我們也可以直接通過FragmentManager的isStateSaved進(jìn)行判斷。不過如果我們使用的是show(FragmentTransaction transaction, String tag)方法,那么就稍微麻煩點(diǎn),下面直接給出

    @Override
    public int show(FragmentTransaction transaction, String tag) {
        try {
            Field field = transaction.getClass().getDeclaredField("mManager");
            field.setAccessible(true);
            if (!checkActivityIsActive((FragmentManager) field.get(transaction))) return STATUS_COMMIT_FAILED;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.show(transaction, tag);
    }

通過事務(wù)反射獲取對(duì)應(yīng)的FragmentManager,然后在調(diào)用之前的判斷方法來判斷是否需要show出來。這樣浸入性不高,另外對(duì)源碼的邏輯改動(dòng)也不多。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容