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

原文來自這里
歡迎轉(zhuǎn)載,但請(qǐng)保留譯者出處:http://www.lxweimin.com/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)

本文旨在說明何種原因何種時(shí)刻這一異常會(huì)被拋出,并且總結(jié)出了幾種建議用于幫你確保你的應(yīng)用再也不會(huì)因?yàn)樗鴆rash。

為什么這異常會(huì)被拋出?

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

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

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

什么時(shí)候這異常會(huì)被拋出?

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

關(guān)于這些輕微的不一致現(xiàn)象存在的原因,則是起源于Honeycomb版本中對(duì)Activity 生命周期的一項(xiàng)重大改變。在Honeycomb之前,Activity 不能被作為可殺死的對(duì)象直到它已經(jīng)暫停過后,意味著onSaveInstanceState()會(huì)在onPause()調(diào)用之前馬上調(diào)用。而從Honeycomb開始,Activity 只能在它已經(jīng)停止過后才能被作為可殺死的對(duì)象,意味著現(xiàn)在onSaveInstanceState()會(huì)在onStop()調(diào)用之前調(diào)用而不是在onPause()調(diào)用之前馬上調(diào)用。這些不同點(diǎn)被總結(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 有時(shí)需要根據(jù)平臺(tái)版本來改變它的行為。舉例來說,在Honeycomb 及其之后的設(shè)備上,每一次commit()onSaveInstanceState()之后調(diào)用都會(huì)拋出一個(gè)異常用于警告開發(fā)者發(fā)生了狀態(tài)丟失。然而,在pre-Honeycomb的設(shè)備上每次發(fā)生狀態(tài)丟失時(shí)就拋出異常將會(huì)帶來過多限制,那些設(shè)備在Activity生命周期要早得多的時(shí)候就會(huì)調(diào)用onSaveInstanceState(),并且更可能發(fā)生意外狀態(tài)丟失。Android 團(tuán)隊(duì)被迫作出妥協(xié):讓舊的平臺(tái)版本有著更好的inter-operation(“for better inter-operation with older versions of the platform”,譯者:這個(gè)不會(huì)翻了Orz),舊設(shè)備不得不與可能發(fā)生于onPause()onStop()之間的意外狀態(tài)丟失共存。support library在兩種平臺(tái)上的不同行為由下表進(jìn)行了總結(jié):

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

如何避免這一異常?

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

  • 在Activity 生命周期方法中提交事務(wù)時(shí)保持小心謹(jǐn)慎 大部分應(yīng)用只會(huì)在最一開始的onCreate()方法之中還有(或者)在對(duì)用戶輸入進(jìn)行反饋的時(shí)候提交事務(wù),這樣一定不會(huì)面臨任何問題。而當(dāng)你的事務(wù)開始冒險(xiǎn)在 Activity 生命周期的其他方法中提交時(shí),像是onActivityResult(),onStart()onResume()之類,事情將變得有些棘手。舉個(gè)例子,你不應(yīng)該在FragmentActivity#onResume()方法中提交事務(wù),因?yàn)檫@個(gè)方法存在幾種當(dāng)Activity 狀態(tài)還沒有被還原時(shí)就被調(diào)用的情況 (see the documentation for more information)。如果你的應(yīng)用需要在Activity 生命周期方法中(非onCreate())提交事務(wù),要么在FragmentActivity#onResumeFragments()中,要么選擇Activity#onPostResume()。這兩個(gè)方法能保證是在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ù) 這一點(diǎn)包括通常使用的方法像是AsyncTask#onPostExecute()LoaderManager.LoaderCallbacks#onLoadFinished()。在這些方法中使用事務(wù)的問題在于,當(dāng)它們被調(diào)用的時(shí)候它們并不具備知曉當(dāng)前Activity 生命周期狀態(tài)的認(rèn)知力。舉個(gè)例子,考慮下面這個(gè)事件序列:

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

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

  • 使用 commitAllowingStateLoss() 僅作為最后手段 調(diào)用commit()commitAllowingStateLoss()之間的惟一區(qū)別僅在于后者不會(huì)拋出異常,即使發(fā)生了狀態(tài)丟失。通常你不會(huì)想要使用這一方法,因?yàn)檫@暗示了你的應(yīng)用中存在發(fā)生狀態(tài)丟失的可能性。更好的方法當(dāng)然是寫好你的應(yīng)用,讓commit()總是在activity 狀態(tài)被保存之前調(diào)用,這將會(huì)達(dá)到更佳的用戶體驗(yàn)。除非狀態(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)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容