Fragment Transactions & Activity State Loss

原文鏈接

StackOverflow的一個問題:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
    at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1341)
    at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1352)
    at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
    at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:574)

解釋為什么會拋出這個異常,并且給出解決這個問題的幾個建議

為什么拋出這個異常

拋出這個異常的原因是試圖在activity的狀態已經保存之后提交FragmentTransaction,導致activity狀態丟失. 在了解詳情之前先來看一看在調用onSaveInstanceState()都做了什么,在Binders & Death Recipients文章中討論了Android應用運行時不好控制,android系統通過殺死進程釋放內存,后臺運行的activity可能在毫無征兆的情況下被殺死. 為了確保用戶的難以預測的行為被隱藏,在activity被銷毀前調用onSaveInstanceState()方法保存activity的狀態. 不管當前的activity有沒有被系統殺死,用戶能在前臺和后臺的activity中無縫連接

調用onSaveInstanceState()時,通過Bundle保存activity的狀態,activity的記錄了dialogs,fragments,views狀態,系統通過代理bundle與服務器端的接口交互,安全的保存了當前的狀態,當系統之后決定恢復activity,它把相同的bundle對象返回給應用,通過bundle恢復之前activity的狀態

那么為什么會拋出異常呢?問題源于這個bundle對象在onSaveInstanceState()被調用時記錄了當前的activity,也就意味著當前在onSaveInstanceState()調用之后調用FragmentTransaction#commit(),這個處理沒有被記住,沒有作為activity的狀態記錄
這個處理將丟失來自用戶對控件的操作,導致UI的狀態丟失. Android為了避免狀態丟失拋出了IllegalStateException異常

什么時候拋出這個異常

如果之前遇到這個問題的話,可能注意到不同的版本拋出的異常有所不同. 越老的設備拋出這個異常的越頻繁,使用support包比使用官方的包出現更多的異常

引起這個問題的原因是在3.1時改變了activity的生命周期,在3.1之前沒有考慮到activity在被暫停之后被殺,意味著onPause()之前調用了onSaveInstanceState()方法. 在3.1時,只考慮了activity在被停止的時候被殺,意味著onSaveInstanceState()onStop()方法之前被調用,而不是在onPause()之前調用,這個不同點在下表中給出總結:

這個小的改變源于activity的生命周期,support包需要根據版本改變行為,在3.1及以上版本,這個異常將在onSaveInstanceState()方法被調用之后,調用commit()方法拋出的,告訴開發者activity的狀態已丟失. 在3.1之前拋出這個異常是限制版本,onSaveInstanceState()方法的調用早于activity的生命周期,結果導致當前的狀態丟失. android團隊做了一個兼容,老設備在onPause()onStop()之間出現狀態丟失,下表給出support包跨版本的情況:

如何避免這個異常

明白什么原因導致狀態丟失后解決這個問題就變得更加容易,希望明白一點support包的工作原理,為什么要在應用中避免狀態丟失是至關重要的,這里給出一些建議保證在應用使用FragmentTransactions正常返回:

  • activity的生命周期內謹慎的使用commit()方法,大多數應用只有在第一次onCreate()時或者響應用戶操作時調用commit,不會拋出這個異常,但是在其他生命周期中使用時可能會拋出這個異常,如onActivityResult(),onStart(),onResume(),不應該在FragmentActivity#onResume()commit,在activity的狀態恢復之前已經調用了這個方法(官網解讀),如果應用中不在onCreate()commit,可以在FragmentActivity#onResumeFragments()或者Activity#onPostResume()commit,這兩個方法都是在activity的狀態恢復之后調用,避免了activity的狀態丟失
  • 避免在接口回掉中執行操作,接口回掉例如AsyncTask#onPostExecute(),LoaderManager.LoaderCallbacks#onLoadFinished(),在這些方法中執行操作會導致狀態丟失,例如下面這個事件:
    1. activity中使用AsyncTask
    2. 點擊home鍵,導致activityonSaveInstanceState()onStop()被調用
    3. AsyncTask#onPostExecute()被調用時,當前的activity已停止
    4. onPostExecute()commit,拋出異常

正常情況下,避免這個異常的最好方式就是避免在回掉中commit,如果應用中一定要在回掉中用的話,不可避免的會調用onSaveInstanceState(),可以使用commitAllowingStateLoss()來解決這個問題

  • 使用commitAllowingStateLoss()方法作為最后的招數,commit()commitAllowingStateLoss()不同點在于在狀態丟失時后者不會拋出異常,通常情況下不會使用這個方法,使用這個方法是就以為著狀態可能會丟失,最好的解決辦法在activity狀態保存之前在應用中commit,除非狀態丟失不可避免,否則就不要用commitAllowingStateLoss()

希望這幾條建議可以幫到你!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容