Fragment提交transaction導致state loss異常

Fragment提交transaction導致state loss異常

PS:由於不清楚原文地址(有很多出處,都沒有貼原文,不清楚原文是哪一篇),故未粘帖。


下面自從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之後
commitonPause()之前 OK OK
commitonPause() 和onStop()之間 STATE LOSS OK
commitonStop()之後 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當前的生命週期。比如,考慮下面的事件序列:

    1. 一個Activity執行一個AsyncTask
    2. 用戶按下Home鍵,導致這個Activity的onSaveInstanceState()onStop() 方法被回調。
    3. AsyncTask完成然後onPostExecute()被調用,而不知道Activity已經處於stopped狀態。
    4. 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上發佈問題,然後將鏈接發表在評論區以便我能夠看到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章