「譯」Fragment事務(wù)與Activity狀態(tài)丟失

原文來自這里
歡迎轉(zhuǎn)載,但請保留譯者出處:http://www.itdecent.cn/p/3d8d78bf38ee

自從Honeycomb(譯者注:Android 3.1)初版發(fā)布以來,如下stack trace與異常信息就讓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)

本文旨在說明何種原因何種時刻這一異常會被拋出,并且總結(jié)出了幾種建議用于幫你確保你的應(yīng)用再也不會因為它而crash。

為什么這異常會被拋出?

這一異常之所以被拋出,是因為你意圖在activity的狀態(tài)被保存后提交一個Fragment事務(wù)(FragmentTransaction),于是引發(fā)了一種命名為Activity狀態(tài)丟失Activity state loss)的現(xiàn)象。在我們深入了解這個詞匯的實際含義前,不妨先看一下當(dāng)onSaveInstanceState()被調(diào)用時究竟發(fā)生了些什么。如我在我的上一篇文章Binders & Death Recipients里說到的那樣,Android應(yīng)用在Android運行時環(huán)境中對于它自身的命運只有非常少的控制權(quán)。而Android系統(tǒng)擁有在任何時刻結(jié)束進程以釋放內(nèi)存的權(quán)限,就結(jié)果來說,后臺activities 可能會被殺死卻收不到一丁點兒警告。為了確保這一偶然發(fā)生的古怪行為對用戶而言不可感知,framework 將給予每一個Activity 一次機會來保存好它的狀態(tài),具體來說就是在將Activity 變得易于被銷毀之前順手調(diào)用一下它的onSaveInstanceState()方法。這樣不管Activity 是否曾經(jīng)被系統(tǒng)殺死過,當(dāng)保存的狀態(tài)在之后被還原,都能讓用戶在前臺后臺之間切換activities時有一種無縫的感受。

當(dāng)framework 調(diào)用onSaveInstanceState()時,就會傳給這個方法一個Bundle對象,而Activity 可以用這個對象來保存它自己的狀態(tài),具體來說Activity 可以在里面保存自己的dialogs, fragments,還有views的狀態(tài)。當(dāng)方法返回時,系統(tǒng)會把這個Bundle打包通過一個Binder 接口傳到System Server process中去,在那里這個Bundle會被保存得很好。當(dāng)系統(tǒng)之后決定重新創(chuàng)建這個Activity時,同樣的Bundle對象就會被傳遞給應(yīng)用,用來讓它能夠還原Activity被殺死之前的狀態(tài)。

所以說為什么這異常會被拋出?嗯,這一問題就只是起源于這樣一個事實而已:Bundle對象在onSaveInstanceState()被調(diào)用后就成為了一個代表Activity 狀態(tài)的快照。這意味著當(dāng)你在onSaveInstanceState()被調(diào)用后調(diào)用FragmentTransaction#commit()的話,那么這一事務(wù)將不會被記住——因為它壓根兒就沒有機會被記錄為Activity 狀態(tài)的一部分了。從用戶視角來看待這一問題,這個事務(wù)的丟失將導(dǎo)致意外的UI狀態(tài)丟失。為了保護用戶體驗,Android 不惜任何代價避免“狀態(tài)丟失”,所以當(dāng)這種事發(fā)生時就會簡單地拋出一個IllegalStateException異常給你。

什么時候這異常會被拋出?

如果你之前已經(jīng)遇到過這一異常了,那么很可能你已經(jīng)注意到在不同的平臺版本之間,這一異常被拋出的概率有一些不一致。舉例來說,你很可能發(fā)現(xiàn)在舊設(shè)備上這一異常拋出得沒有這么頻繁,或是你的應(yīng)用在使用support library時會比使用official framework classes時更有可能發(fā)生crash。這些輕微的不一致現(xiàn)象讓許多人猜測support library存在bug不可信賴。然而,這種猜測基本上是不正確的。

關(guān)于這些輕微的不一致現(xiàn)象存在的原因,則是起源于Honeycomb版本中對Activity 生命周期的一項重大改變。在Honeycomb之前,Activity 不能被作為可殺死的對象直到它已經(jīng)暫停過后,意味著onSaveInstanceState()會在onPause()調(diào)用之前馬上調(diào)用。而從Honeycomb開始,Activity 只能在它已經(jīng)停止過后才能被作為可殺死的對象,意味著現(xiàn)在onSaveInstanceState()會在onStop()調(diào)用之前調(diào)用而不是在onPause()調(diào)用之前馬上調(diào)用。這些不同點被總結(jié)在了如下表格中:

pre-Honeycomb post-Honeycomb
Activities can be killed before onPause()? NO NO
Activities can be killed before onStop()? YES NO
onSaveInstanceState(Bundle) is guaranteed to be called before... onPause() onStop()

作為Activity 生命周期重大改變的結(jié)果,support library 有時需要根據(jù)平臺版本來改變它的行為。舉例來說,在Honeycomb 及其之后的設(shè)備上,每一次commit()onSaveInstanceState()之后調(diào)用都會拋出一個異常用于警告開發(fā)者發(fā)生了狀態(tài)丟失。然而,在pre-Honeycomb的設(shè)備上每次發(fā)生狀態(tài)丟失時就拋出異常將會帶來過多限制,那些設(shè)備在Activity生命周期要早得多的時候就會調(diào)用onSaveInstanceState(),并且更可能發(fā)生意外狀態(tài)丟失。Android 團隊被迫作出妥協(xié):讓舊的平臺版本有著更好的inter-operation(“for better inter-operation with older versions of the platform”,譯者:這個不會翻了Orz),舊設(shè)備不得不與可能發(fā)生于onPause()onStop()之間的意外狀態(tài)丟失共存。support library在兩種平臺上的不同行為由下表進行了總結(jié):

pre-Honeycomb post-Honeycomb
commit() before onPause() OK OK
commit() between onPause() and onStop() STATE LOSS OK
commit() after onStop() EXCEPTION EXCEPTION

如何避免這一異常?

一旦你理解實際上究竟發(fā)生了什么,那么避免Activity 狀態(tài)丟失就變得整個都容易起來。如果你已經(jīng)達到了這篇文章里提到的高度,希望你能對于support library如何工作還有為什么避免你的應(yīng)用發(fā)生狀態(tài)丟失是如此的重要理解得更好。當(dāng)你在你的應(yīng)用中使用FragmentTransaction時,萬一你是在搜索快速解決方案時參考到這篇文章,這里有幾條建議需要你記在腦海之中:

  • 在Activity 生命周期方法中提交事務(wù)時保持小心謹慎 大部分應(yīng)用只會在最一開始的onCreate()方法之中還有(或者)在對用戶輸入進行反饋的時候提交事務(wù),這樣一定不會面臨任何問題。而當(dāng)你的事務(wù)開始冒險在 Activity 生命周期的其他方法中提交時,像是onActivityResult(),onStart()onResume()之類,事情將變得有些棘手。舉個例子,你不應(yīng)該在FragmentActivity#onResume()方法中提交事務(wù),因為這個方法存在幾種當(dāng)Activity 狀態(tài)還沒有被還原時就被調(diào)用的情況 (see the documentation for more information)。如果你的應(yīng)用需要在Activity 生命周期方法中(非onCreate())提交事務(wù),要么在FragmentActivity#onResumeFragments()中,要么選擇Activity#onPostResume()。這兩個方法能保證是在Activity還原至原先狀態(tài)后才被調(diào)用,因此都能避免狀態(tài)丟失的可能性(As an example of how this can be done, check out my answer to this StackOverflow question for some ideas on how to commit FragmentTransactions in response to calls made to the Activity#onActivityResult() method)。
  • 避免在異步回調(diào)方法中使用事務(wù) 這一點包括通常使用的方法像是AsyncTask#onPostExecute()LoaderManager.LoaderCallbacks#onLoadFinished()。在這些方法中使用事務(wù)的問題在于,當(dāng)它們被調(diào)用的時候它們并不具備知曉當(dāng)前Activity 生命周期狀態(tài)的認知力。舉個例子,考慮下面這個事件序列:

    1. 一個activity 啟動了一個AsyncTask
    2. 用戶按下"Home",這將令activity 的onSaveInstanceState()還有onStop()方法被調(diào)用
    3. 那個AsyncTask這時完成了,于是其onPostExecute()方法在不知曉activity 已經(jīng)停止的情況下被調(diào)用了
    4. 因為一個FragmentTransaction在onPostExecute()中被提交,造成了異常被拋出

    總的來說,在這樣的例子中避免異常的最佳方法無過于簡單地避免在異步回調(diào)方法中提交事務(wù)。Google工程師似乎也很贊同這一信條。根據(jù)這篇Android Developers group的文章,Android 團隊表示能夠在異步回調(diào)方法中提交FragmentTransaction造成UI變換將會是糟糕的用戶體驗。如果你的應(yīng)用需要在這些回調(diào)方法中提交事務(wù),那么并沒有簡單的方法能夠確保這些回調(diào)不會是在onSaveInstanceState()之后才被調(diào)用,你也許不得不求助于使用commitAllowingStateLoss(),同時還要處理可能發(fā)生的狀態(tài)丟失(See also these two StackOverflow posts for additional hints, here and here)。

  • 使用 commitAllowingStateLoss() 僅作為最后手段 調(diào)用commit()commitAllowingStateLoss()之間的惟一區(qū)別僅在于后者不會拋出異常,即使發(fā)生了狀態(tài)丟失。通常你不會想要使用這一方法,因為這暗示了你的應(yīng)用中存在發(fā)生狀態(tài)丟失的可能性。更好的方法當(dāng)然是寫好你的應(yīng)用,讓commit()總是在activity 狀態(tài)被保存之前調(diào)用,這將會達到更佳的用戶體驗。除非狀態(tài)丟失存在著無可避免的可能性,要么就不應(yīng)該使用commitAllowingStateLoss()。

Hopefully these tips will help you resolve any issues you have had with this exception in the past. If you are still having trouble, post a question on StackOverflow and post a link in a comment below and I can take a look. :)

As always, thanks for reading, and leave a comment if you have any questions. Don't forget to +1 this blog and share this post on Google+ if you found it interesting!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容