下面自從Honeycomb發布后,下面棧跟蹤信息和異常信息已經困擾了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的狀態已經保存后commit一個FragmentTransaction ,導致了一個現象叫做Activity state loss。在我們深入細節之前,讓我們先看看在onSaveInstanceState()調用后發生了什么。在我的前一篇 Binders & Death Recipients有討論到,Android應用程序在Android 運行時系統中只有很小的控制權。Android系統為了釋放內存可以在任意時刻停止進程,然后處于后臺的Activity就會被毫無警告地殺掉。為了保證有時候因此引起的不穩定行為能避免用戶知道,Android框架給每一個Activity通過調用onSaveInstanceState()來保存自己狀態的機會,它會在Activity可能被銷毀之前調用。當后面恢復狀態的時候,用戶不會感覺到Activity已經被系統殺掉了,而會感覺前臺和后臺的Activity無縫切換。
當Android框架調用onSaveInstanceState(),它將一個Bundle對象通過這個方法傳遞,以便Activity后面恢復狀態。Activity可以將它的Dialog、fragment以及view的狀態保存在Bundle中。當這個方法返回的時候,系統通過Binder結果打包Bundle對象然后傳給系統服務進程。系統服務進程負責保證Bundle對象安全地保存下來。當系統后面決定重新創建Activity的獲釋后,它就會將相同的Bundle對象發揮應用程序,以便于用它來回復舊的Activity狀態。
所以為什么這個異常隨后拋出?這個問題導致的原因是因為那些Bundle對象代表Activity在onSaveInstanceState()被調用那時候的一個快照,沒更多了。這就意味著當你在onSaveInstanceState()之后調用FragmentTransaction#commit()的時候,transation不會被記錄。因為它不會作為之前Activity的狀態被保存。從用戶的角度來說,這個transaction就像丟失了,導致UI狀態意外的丟失。為了保證用戶體驗,Android不計一切代價避免狀態丟失,也就是當它發生的時候簡單地拋出一個IllegalStateException。
這個異常什么時候會拋出?
如果你之前已經碰到過這個異常,你可能會注意到異常拋出的時機因為不同的Android版本而不一致。比如,如可能會發現老版本的設備上,這個異常拋出比較不頻繁,或者當你的程序中使用support library而不是官方框架中的類時更容易觸發這個異常。這些輕微的一致讓很多人都以為support library有bug,不值得信任。然而,這些假設都不是正確的。
這些輕微的不一致是因為在Honeycomb版本中的Activity生命周期有了重要的變化。Honeycomb之前的版本,activity被認為在pause之前都不會被殺掉,這意味著onSaveInstanceState()會在onPause()之前被調用。從HoneyComb開始,Activity被認為只會在stopped只會被殺掉,意味著onSaveInstanceState()現在會在onStop()之前被調用而不是在onPause()之前。這些變化在下表中總結:
Honeycomb之前 | Honeycomb之后 | |
---|---|---|
Activity是否可以在onPause()之前被殺掉? | NO | NO |
Activity是否可以在onStop()之前被殺掉? | YES | NO |
onSaveInstanceState(Bundle) 保證在...之前被調用 | onPause() | onStop() |
由于Activity生命周期的輕微變化,support library有時候需要根據系統版本選擇他的行為。比如,在Honeycomb及以上設備,每次在onSaveInstanceState()之后調用commit()都會拋出一個異常,以便警告開發者已經發生了狀態丟失。然而,在每次這種情況拋出異常在Honeycomb之前的設備上就顯得太具有限制性了,它們的onSaveInstanceState()調用發生在Activity生命周期中更早的一段時期,并且更容易導致意外的狀態丟失。Android團隊被迫做出妥協:為了更好地跟老版本兼容,舊設備可能必須要忍受在onPause()和onStop()之間意外的狀態丟失。Support library在不同兩個版本的行為如下表總結:
Honeycomb之前 | Honeycomb之后 | |
---|---|---|
commit在onPause()之前 | OK | OK |
commit在onPause() 和onStop()之間 | STATE LOSS | OK |
commit在onStop()之后 | EXCEPTION | EXCEPTION |
怎么避免這個異常?
一旦你懂得了真正發生了什么,避免Activity狀態丟失就簡單多了。如果你已經在讀這篇文章之間就已經解決過這個問題了,希望你能對support library有一個更深的了解,并且知道為什么避免狀態丟失對你的程序這么重要。為了方便你通過這篇文章尋找快速的解決方案,這里有一些建議希望你記得在使用FragmentTransactions的時候使用:
-
在Activity生命周期方法中commit transation的時候一定要小心。很多應用程序只會在onCreate()或者為了響應用戶輸入的時候調用一次,所以他們不會遇到任何問題。然而,當你的transation開始冒險在其他的生命周期(比如onActivityResult(),onStart(),onResume() )中commit的時候,事情就可能變得棘手了。比如,你不應該在FragmentActivity#onResume() 方法中commit transation,為了避免有些時候這個方法在Activity的狀態恢復之前被調用( 查看文檔,了解更多)。如果你的應用程序需要在處理onCreate()之外的生命周期方法中commit transation,在FragmentActivity#onResume() 或者Activity#onPostResume()中調用。這兩個方法會被保證在Activity恢復它的狀態之后調用,因此會避免可能的狀態丟失。(一個關于如何去做的例子,可以查看我在StackOverFlow上的回答。這個回答設計怎么正確地響應Activity#onActivityResult()方法,然后commit FragmentTransactions)
-
避免是異步調用方法中執行transactions 。這個包括經常被使用的方法比如AsyncTask#onPostExecute() 和 LoaderManager.LoaderCallbacks#onLoadFinished() 。在這些方法中執行transactions會有問題,因為他們當這些方法被回調的時候,他們不知道Activity當前的生命周期。比如,考慮下面的事件序列:
- 一個Activity執行一個AsyncTask
- 用戶按下Home鍵,導致這個Activity的onSaveInstanceState()和onStop() 方法被回調。
- AsyncTask完成然后onPostExecute()被調用,而不知道Activity已經處于stopped狀態。
- 在onPostExectute()方法中的FragmentTransaction被committed,導致一個異常被拋出。
總之,在這些案例中避免異常拋出的最優方法就是避免在異步回調方法中commit transactions。Google工程師似乎同意這個見解。根據在Android Develop group上的這篇文章,Android開發團隊認為通過commit FragmentTransactions來讓UI產生重大的變化對用戶體驗十分不友好。如果你的應用程序需要在這些回調方法中執行transaction,那么沒有什么簡單方法可以保證這些回調不會再onSaveInstanceState()后調用,你可能必須使用commitAllowStateLoss()并且處理可能發生的狀態丟失。(詳見兩篇StackOverFlow文章,文章1、文章2)
- 只使用commitAllowingStateLoss()作為最后的解決方案。commit()和commitAllowingStateLoss()唯一的區別是后者在狀態丟失的時候不會拋出異常。通常你不會想使用這個方法因為它意味著狀態丟失可能發生。更好的解決方案當然是修改你的程序以便commit()被保證在activity的狀態被保存前調用,因為這樣可能會讓用戶體驗更好。除非狀態丟失是不可避免的,否則commitAllowingStateLoss()就不應該被使用。
希望這些建議能夠幫助你解決以往因為這個異常而引起的問題。如果你還有問題,在StackOverflow上發布問題,然后將鏈接發表在評論區以便我能夠看到。
譯文鏈接Fragment transaction commit state loss
文章列表