Fragment的事務操作&Actvity的狀態丟失

Fragment Transactions & Activity State Loss

本文翻譯自Fragment Transactions & Activity State Loss

下面所示的異常堆棧追蹤在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)`

這篇文章就是來解釋這個異常發生的原因和異常拋出的時機,並且給出了一些有益的建議來避免這個異常的發生

爲什麼會拋出這個異常?

這個異常的拋出是由於你準備在actvity的狀態已經被保存後來做一次FragmentTransaction的commit,這將會導致Actvity state loss的現象的出現。

在我們深入討論這個現象之前,我們先來看一下在onSaveInstanceState()方法調用後發生了什麼。正如我在上一篇博文中提到的http://www.androiddesignpatterns.com/2013/08/binders-death-recipients.html,在android runtime 期間 android應用自己幾乎不能控制自己,android系統有權在任何時候釋放內存,因此後臺的actvity也會被毫無徵兆的kill掉。爲了確保這種無法估計的行爲對用戶是無感知的,framework給予每個activity在覺察自己可能(back鍵除外)會被銷燬時調用onSaveInstanceState()來保存自己的狀態,用戶在前後臺切換actvity時,保存的狀態數據將會被恢復時,而不會覺察到這個activity是否已經是被系統kill的,在用戶看來這是“無縫”切換的。
當framework調用onSaveInstanceState(),它傳遞一個Bundle對象給actvity來保存它的diaglog、fragments、view的信息
。當方法回調時,系統通過Binder接口傳遞這個Bundle對象到System Sever,在這裏Bundle對象將被安全的存儲。然後系統之後重建actvity時再將剛纔的Bundle傳遞給應用,這時actvity得到之前保存的狀態

鋪墊只是解釋完之後下面將詳細解釋爲什麼會拋出這個異常。這個問題源於這樣一個事實,傳遞的Bundle對象代表着activity在調用onSaveInstanceState()這一時刻的肖像刻畫,這就意味着,在onSaveInstanceState()之後調用FragmentTransaction#commit()這個transaction將不會被記錄,因爲它一開始就沒有被記錄在Activity的狀態中。從用戶看來,actvity切換恢復時這個transaction體現爲丟失的,這將導致Activity的UI狀態丟失。爲了保護用戶的體驗,Android不惜一切代價避免狀態丟失,出現時就拋出一個異常來提醒開發者。

何時拋出這個異常

如果你之前遇到過這個異常,你可能會注意到,這個異常的拋出隨着平臺不同的而變得有點不一致。舉例來說,你可能會發現老舊的機器拋出這個異常更少些,或者使用support library比官方的framework更容易出現這個異常。這些輕微的矛盾導致了一些諸如“support library 有bug,不可信”的論調,然而這通常都不是真的。

這些輕微矛盾的出現是源於在Honeycomb中Activity生命週期的變化,在Honeycomb之前,actvity在調用OnPause方法之後才能被killed的,這就意味着onSaveInstanceState()需要在在OnPause之前調用。而在Honeycomb版本時,actvity只有在onStop之後才能被killed,這也就意味着onSaveInstanceState()需要在在OnStop之前調用而不是以前版本中的OnPause之前調用

Activity生命週期的微小改變,將使得support library有時需要基於平臺來改變它的一些行爲,舉例來說,在Honeycomb及其以上的設備中,每次在onSaveInstanceState()之後commit都會拋出這個異常,然而在Honeycomb之前的設備中異常就會出現少一些。android團隊被迫做出讓步:爲了老版本的內在更好地兼容,老的設備不得不忍受在 onPause() and onStop()之間的狀態丟失support library的行爲在不同平臺之間總結如下

如何避免這個異常

當你明白到底真正發生了什麼你就會發現activity避免狀態丟失是多麼簡單。如果你在這篇博文中做到這一步,希望你能明白整個
support library是如何工作的,並且爲什麼避免狀態丟失是如此重要。爲了方便你在這篇博文尋找一個快速修復的方法,這裏有一些建議需要牢牢記住,當你在應用中使用FragmentTransactions的時候

  • 在Actvity的生命週期中commit transactions需要小心謹慎

大多數應用只會在在onCreate中第一次commit transaction,這當然不會遇到這種問題,然而當你的transaction企圖在Activity生命週期其他方法,諸如onActivityResult(), onStart(), and onResume()中commit時,這時事情就變得棘手了。舉例來說,你不該在 FragmentActivity#onResume()中commit transaction,這是由於有一些情況,這個方法有時候會在activity狀態保存前調用(參考developer.android.com/reference/android/support/v4/app/FragmentActivity.html#onResume())。因此如果你的應用需要在Activity生命週期onCreate()之外commit transaction,只在FragmentActivity#onResumeFragments() orActivity#onPostResume()方法中去commit,這兩個方法能夠保證Activty狀態保存之後再調用,這樣就避免了狀態丟失(參考http://stackoverflow.com/questions/16265733/failure-delivering-result-onactivityforresult)

  • 避免在異步中執行 commit transactions

這包括了常用的一些方法,諸如:AsyncTask#onPostExecute() and LoaderManager.LoaderCallbacks#onLoadFinished()等。由於這些方法根本不知道在當前Activity哪一個生命週期的去調用了他們,因而在其中commit transaction就會出問題。比如

An activity executes an AsyncTask.

The user presses the “Home” key, causing the activity’s onSaveInstanceState() and onStop() methods to be called.

The AsyncTask completes and onPostExecute() is called, unaware that the Activity has since been stopped.

A FragmentTransaction is committed inside the onPostExecute() method, causing an exception to be thrown.

總而言之就是要在異步回調方法中避免去commit transaction來避免這個異常的發生。谷歌工程師看起來也是同意這種觀念的。通過android gruop的這個文章(https://groups.google.com/d/msg/android-developers/dXZZjhRjkMk/QybqCW5ukDwJ) ,谷歌android team 認爲通過異步commit transaction 帶來的UI變化並不利於用戶體驗。如果你的應用不得不在異步中提交,你將不得不使用commitAllowingStateLoss()並且處理好狀態可能丟失的發生的情形(http://stackoverflow.com/questions/7992496/how-to-handle-asynctask-onpostexecute-when-paused-to-avoid-illegalstateexceptionhttp://stackoverflow.com/questions/8040280/how-to-handle-handler-messages-when-activity-fragment-is-paused

  • 將 commitAllowingStateLoss()作爲最後使用的手段

commit和commitAllowingStateLoss唯一的區別就是後者在狀態丟失時候不會拋出異常,通常不會使用這個方法,因爲這將意味着狀態的可能丟失。最好的方式就是寫好你的應用,這樣transaction確保在activity狀態保存之後commit,這樣用戶體驗會更好。除非狀態丟失不可避免了,否則不要使用commitAllowingStateLoss()

發佈了170 篇原創文章 · 獲贊 143 · 訪問量 41萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章