Fragment commit 的正確姿勢

MadisonRong發表於2017-11-24

今天工作的時候在 bugly 上看到一個奔潰分析中有這麼一個問題:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

於是就開始 Google,發現這問題由來已久,早在11年的時候就已經有人在提出這個問題。(時至今日仍出現這問題,不得不說 Google 你是可以的)

在 Stack Overflow 上的這篇文章中也說了一些奇技淫巧去解決問題,但是回答者也並不理解為什麼會這樣,只知道這樣是可以解決的。而問題出現的原因其實也可以猜測出來,就是在 activity 多次的進行前後臺切換後,導致了 fragment 的 commit 操作發生在 activity 的 onSaveInstanceState 之後。

本著探索一下的精神,開始看原始碼。首先看了 commit 的原始碼。戳開 commit 後,看到了 FragmentTransaction 這個抽象類。在原始碼的最後可以看到,這裡定義了四個跟 commit 有關的方法。(由於英文比較渣,單看英文的註釋似乎有點難以理解四個方法之間的差異,於是又去 Google 了一下,然後發現了這篇文章,有點茅塞頓開的感覺)

ok,接著繼續來看 commit 的原始碼。
BackStackRecord這個類裡看到了具體實現如下:

    @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;
    }複製程式碼

從上面可以發現,關鍵在於enqueueAction這個方法上,於是繼續跟,在FragmentManager的原始碼發現如下:

    private void checkStateLoss() {
        if (mStateSaved) {
            throw new IllegalStateException(
                    "Can not perform this action after onSaveInstanceState");
        }
        if (mNoTransactionsBecause != null) {
            throw new IllegalStateException(
                    "Can not perform this action inside of " + mNoTransactionsBecause);
        }
    }

    /**
     * Adds an action to the queue of pending actions.
     *
     * @param action the action to add
     * @param allowStateLoss whether to allow loss of state information
     * @throws IllegalStateException if the activity has been destroyed
     */
    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();
        }
    }複製程式碼

從上面可以看出,有三種情況會導致異常:

  1. Can not perform this action after onSaveInstanceState
  2. Can not perform this action inside of xxx
  3. Activity has been destroyed

針對方法:

  1. 使用 commitAllowingStateLoss 方法
  2. 避免在非同步回撥裡進行 fragment transaction 的操作(可以參考這裡最後幾點)
  3. 把 commit 操作包含在 !isFinishing() 內部(這裡給出瞭解決方案)

所以,個人比較推薦的 commit 姿勢應該是:

if (!isFinishing()) {
        getSupportFragmentManager().beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .commitAllowingStateLoss();
}複製程式碼

(這裡說明一下,commitAllowingStateLoss方法不是一定要這樣的,就像在參考文章裡說的,當你確定你無法避免那個異常的時候才用這個,官方推薦的是用 commit;而使用commitAllowingStateLoss導致的後果在參考文章裡也有說明)

以上,純粹是個人的小總結,也歡迎朋友們對文中不對的地方進行指正。Peace~~
(最後再嘮嗑一句,英語過得去並且有耐心的朋友強烈推薦看一下參考文獻的第一篇,講得比較詳細)

參考文獻

  1. www.androiddesignpatterns.com/2013/08/fra…
  2. stackoverflow.com/questions/7…
  3. blog.chengyunfeng.com/?p=1016
  4. stackoverflow.com/questions/9…

相關文章