原文來自這里
歡迎轉(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 theActivity#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è)事件序列:- 一個(gè)activity 啟動(dòng)了一個(gè)
AsyncTask
- 用戶按下"Home",這將令activity 的
onSaveInstanceState()
還有onStop()
方法被調(diào)用 - 那個(gè)
AsyncTask
這時(shí)完成了,于是其onPostExecute()
方法在不知曉activity 已經(jīng)停止的情況下被調(diào)用了 - 因?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)。 - 一個(gè)activity 啟動(dòng)了一個(gè)
-
使用
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!