從源碼角度理解Can not perform this action after onSaveInstanceState異常


在開發中經常遇到Fatal Exception: java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState異常,那這個異常出現原因是什么呢,怎么解決呢?

問題描述
出現Fatal Exception: java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState異常有兩種情況:

  1. FragmentTransaction的commit()時出現:
    具體堆棧信息如下:

  2. Activity/FragmentActivity的onBackPressed時出現:
    具體堆棧信息如下:

問題原因和解決方法
出現Fatal Exception: java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState異常細分為兩種情況,但產生原因是一樣的,都是因為在存儲狀態之后調用了commit()/onBackPressed()方法,在commit()/onBackPrssed()中會調用checkStateLoss()方法,具體如下:

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

其中如果this.mStateSaved為true,就會拋出這個異常。而對於mStateSaved用了保存Fragment狀態,是在Activity#onSaveInstanceState時通過調用FragmentManager#saveAllState方法,來進行Fragment的狀態保存,同時設置mStateSaved為true,用來標識狀態已被保存過。下面具體分析兩種情況:
1.FragmentTransaction的commit()時出現:
FragmentTransaction的commit()會調用BackStackRecord.java的commit()方法,可以看到commit()和commitAllowingStateLoss()的具體實現,如下:
BackStackRecord.java

@Override
    public int commit() {
        return commitInternal(false);
    }

    @Override
    public int commitAllowingStateLoss() {
        return commitInternal(true);
    }
    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;
    }

可以看出commit()和commitAllowingStateLoss()都是調用commitInternal()方法,只是傳參不一樣,而在commitInternal()方法中,根據傳參會調用FragmentManager的enqueueAction方法,具體如下:

public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
        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();
        }
    }

如果allowStateLoss為true的話會調用checkStateLoss()方法進行檢測。所以從源碼可知commit()和commitAllowingStateLoss()的區別在於,commit()方法會檢測fragment的狀態,而commitAllowingStateLoss()不會對狀態進行檢測,狀態會丟失。
由此得到結論和解決方法:
(1)在activity的生命周期方法中提交事務要小心,越早越好,比如onCreate或是在接收用戶的輸入時來提交。盡量避免在onActivityResult()方法中提交。
(2)避免在異步的回調方法中執行commit。因為他們感知不到當前activity生命周期的狀態。
(3)如果ui狀態的改變對用戶來說是可以接受的話使用commitAllowingStateLoss()代替commit()。
2.Activity/FragmentActivity的onBackPressed時出現:
按返回鍵時會調用super.onBackPressed()方法,如下:
FragmentActivity.java中

@Override
    public void onBackPressed() {
        if (!mFragments.getSupportFragmentManager().popBackStackImmediate()) {
            super.onBackPressed();
        }
    }

會調用FragmentManagerImpl的popBackStackImmediate()方法,如下:

@Override
    public boolean popBackStackImmediate() {
        checkStateLoss();
        return popBackStackImmediate(null, -1, 0);
    }

會調用checkStateLoss()方法,同上,會檢測狀態。
那這個問題怎么解決呢?
(1)添加try…catch,感覺這種方法並沒有解決根本問題,不推薦。
(2)重寫onSaveInstanceState方法,不調用super,但onSaveInstanceState方法是用來存儲狀態使用的,不調用super,防止系統保存fragment的狀態,可能會引發一引起其他的問題;再有就是,對於support包下,在其onStop時也會把mStateSaved置為true,仍然能夠遇到state loss exception。不推薦。
(3)設置標志位,狀態保存過后,不處理KEY事件。具體如下:

public class FragmentStateLossActivity extends Activity {
    private static final String TAG = "Fragment state loss";
    private boolean mStateSaved;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fragment_state_loss);
        mStateSaved = false;
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        // Not call super won't help us, still get crash
        super.onSaveInstanceState(outState);
        imStateSaved = true;
    }

    @Override
    protected void onResume() {
        super.onResume();
        mStateSaved = false;
    }

    @Override
    protected void onStop() {
        super.onStop();
        mStateSaved = true;
    }

    @Override
    protected void onStart() {
        super.onStart();
        mStateSaved = false;
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (!mStateSaved) {
            return super.onKeyDown(keyCode, event);
        } else {
            // State already saved, so ignore the event
            return true;
        }
    }

    @Override
    public void onBackPressed() {
        if (!mStateSaved) {
            super.onBackPressed();
        }
    }
}

但這種方法雖然能從根本上解決crash,但相對比較麻煩。
目前還沒有發現更好的方法。

參考
https://juejin.im/entry/58636864128fe10069efc4b5
https://huxian99.github.io/2016/08/28/cj3qymo360000owxk9zp17alo/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM