一個關於 DialogFragment 狀態引起的崩潰日誌

問題復現

最近出現了一個很奇怪的問題,問題異常日誌以下:java

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:2053)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:2079)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:678)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:632)
at android.support.v4.app.DialogFragment.dismissInternal(DialogFragment.java:223)
at android.support.v4.app.DialogFragment.dismiss(DialogFragment.java:190)
複製代碼

剛開始覺得是 DialogFragment 調用 show 的時候,沒有判斷 Activity 是否已經 Finish 了。結果發如今項目代碼裏是作了判斷的,才知道確定沒這麼簡單。android

FragmentManagerImpl.checkStateLossbash

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);
     }
}
複製代碼

FragmentManagerImpl. enqueueAction服務器

/**
 * 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) {
    //注意這裏的allowStateLoss 它決定了checkStateLoss方法是否進行
    if (!allowStateLoss) {
        checkStateLoss();
    }
    synchronized (this) {
        if (mDestroyed || mHost == null) {
            throw new IllegalStateException("Activity has been destroyed");
        }
        if (mPendingActions == null) {
            mPendingActions = new ArrayList<>();
        }
        mPendingActions.add(action);
        scheduleCommit();
    }
}
複製代碼

BackStackRecord.commitInternalapp

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;
}
複製代碼

BackStackRecord.commitide

public int commit() {
    //這裏傳入的就是allowStateLoss參數
    return commitInternal(false);
}
複製代碼

DialogFragment.showui

public void show(FragmentManager manager, String tag) {
    mDismissed = false;
    mShownByMe = true;
    FragmentTransaction ft = manager.beginTransaction();
    ft.add(this, tag);
    //沒想到的時候DialogFragment的show方法會調用commit() 導火索
    ft.commit();
}
複製代碼

目前來看是由於 DialogFragment 的 show 方法致使強行檢查 Fragment 的狀態,而剛好在檢查的時候狀態已經被保存致使 FragmentManager 的標記位 mStateSaved = true 。this

情景再現

當用戶在進入首頁模塊時直接按 Home 鍵或者遇到內存不足等特殊狀況下觸發了onSaveInstanceState 致使 FragmentManager 進行「智能」保存當前 Activity 中 Fragment 的狀態,由於 DialogFragment (廣告彈窗)須要先從服務器拉取數據,因此以後在 show 的時候去 checkStateLoss 時觸發了異常。spa

解決方案

  • 一、將廣告彈窗實現改成純 Dialog ,再也不使用 DialogFragment
  • 2若是本身的項目裏面不須要對 Fragment 的相關狀態進行保存和維護,能夠在相關 Activity 中複寫 onSaveInstance 不進行 super 回調便可(不建議使用此方案);
  • 三、可重寫 DialogFragment 中的 show 方法,並使用 commitAllowStateLoss 提交,以下代碼(推薦使用此方案)。
// 方案3代碼示例
    override fun show(manager: FragmentManager?, tag: String?) {
        // super.show(manager, tag)
        val ft = manager?.beginTransaction()
        ft?.add(this, tag)
        ft?.commitAllowingStateLoss()
    }
複製代碼
相關文章
相關標籤/搜索