原始碼分析commitAllowingStateLoss() 和commit()的區別
之前在使用Fragment的時候偶爾會有這麼一個報錯,Can not perform this action after onSaveInstanceState,意思為無法再onSaveInstanceState之後執行該操作,這個操作就是指commit(),之前也沒怎麼在意,後來通過檢視原始碼去了解了一下這個問題,以下是對這個問題的解析及對應解決辦法的對比。
Fragment是我們經常用到的東西,常用的操作有新增(add)、移除(remove)、替換(replace)等,這些操作組成一個集合transaction,我們在通過呼叫getSupportFragmentManager().beginTransaction()來獲取這個FragmentTransaction類的例項來管理這些操作,將他們存進由activity管理的back stack中,這樣我們就可以進行fragment變化的回退操作,也可以這樣去獲取FragmentTransaction類的例項:
FragmentManager mFragmentManager = getSupportFragmentManager(); FragmentTransaction mFragmentTransaction = mFragmentManager.beginTransaction(); 為什麼我們會有這種報錯呢,因為我們在使用add(),remove(),replace()等方法將Fragment的變化新增進去,然後在通過commit去提交這些變化(另外,在commit之前可以去呼叫addToBackState()方法,將這些變化加入到activity管理的back stack中去,這樣使用者呼叫返回鍵就可以回退這些變化了),提交完成之後這些變化就會應用到我們的Fragment中去。但是,這個commit()方法,你只能在avtivity儲存他的狀態之前呼叫,也就是onSaveInstanceState(),我們都知道activity有一個儲存狀態的方法和恢復狀態的方法,這個就不詳細解釋了,在onSaveInstanceState()方法之後去呼叫commit(),就會丟擲我們遇到的這個異常,這是因為在onSaveInstanceState()之後呼叫commit()方法,這些變化就不會被activity儲存,即這些狀態會被丟失,但我們可以去用commitAllowingStateLoss()這個方法去代替commit()來解決這個為題,下面我們通過原始碼去看這兩個方法的區別。 首先從我們獲取FragmentTransaction類的例項開始,即getSupportFragmentManager(),原始碼是這樣的:
/** * Return the FragmentManager for interacting with fragments associated * with this activity. */ public FragmentManager getSupportFragmentManager() { return mFragments; } 而這個返回的mFragments是一個FragmentManagerImpl類 的例項,他繼承自FragmentManager這個類: final FragmentManagerImpl mFragments = new FragmentManagerImpl(); 我們在FragmentManager這個類中還看到beginTransaction()這個抽象方法,開啟他的實現類 final class FragmentManagerImpl extends FragmentManager { ... ... @Override public FragmentTransaction beginTransaction() { return new BackStackRecord(this); } .... ... } 我們看到這個實現類中的該方法是返回一個BackStateRecord類的實體,我們繼續去追蹤這個類,就會發現,這個類其實是繼承自FragmentTransaction的,並且,我們在這裡看到我們熟悉的方法: final class BackStackRecord extends FragmentTransaction implements FragmentManager.BackStackEntry, Runnable { public BackStackRecord(FragmentManagerImpl manager) { mManager = manager; } public int commit() { return commitInternal(false); } 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); } mCommitted = true; if (mAddToBackStack) { mIndex = mManager.allocBackStackIndex(this); } else { mIndex = -1; } mManager.enqueueAction(this, allowStateLoss); return mIndex; } } 終於找到了我們有用的東西了,這裡省略了其他不必要的程式碼,只留下我們需要用的核心程式碼,有興趣可以自己去檢視原始碼,這裡還有省略掉我們常用的add、remove、replace等方法,迴歸正題,看我們要找的兩個方法commit()和commitAllowingStateLoss(),他們都呼叫了commitInternal(boolean allowStateLoss)這個方法,只是傳入的引數不同,我們去看commitInternal方法,該方法首先去判斷是否已經commit,這個簡單,直接跳過,最後他執行的是FragmentTransactionImpl類的enqueueAction方法,好,不要嫌麻煩,我們繼續去追蹤這個方法: public void enqueueAction(Runnable action, boolean allowStateLoss) { if (!allowStateLoss) { checkStateLoss(); } synchronized (this) { if (mActivity == null) { throw new IllegalStateException("Activity has been destroyed"); } if (mPendingActions == null) { mPendingActions = new ArrayList<Runnable>(); } mPendingActions.add(action); if (mPendingActions.size() == 1) { mActivity.mHandler.removeCallbacks(mExecCommit); mActivity.mHandler.post(mExecCommit); } } } 通過這個方法我們可以看到,他首先去根據commit和commitAllowingStateLoss這兩個方法傳入的引數不同去判斷,然後將任務扔進activity的執行緒佇列中,這裡我們重點去看的是checkStateLoss()這個方法: 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); } } 這個方法很簡單,就只是一個簡單的判斷而已,並且只有呼叫commit方法才會執行這裡,commitAllowingStateLoss則直接跳過這步,這裡我們呼叫commit方法時,系統系判斷狀態(mStateSaved)是否已經儲存,如果已經儲存,則丟擲"Can not perform this action after onSaveInstanceState"異常,這就是我們遇到的問題了,而用commitAllowingStateLoss方法則不會這樣,這就與我們之前分析的activity去儲存狀態對應上了,在activity儲存狀態完成之後呼叫commit時,這個mStateSaved就是已經儲存狀態,所以會丟擲異常。 長篇大論終於講完了,其實回頭一看並沒有多麼複雜,就跟著原始碼一步一步去找,就會找到我們發生錯誤的地方,看了原始碼之後發現,原來並沒有多麼難,so easy!哈哈