Fragment Transactions和Activity狀態丟失

下面的堆棧跟蹤和異常代碼,自從Honeycomb的初始發行版本就一直使得StackOverflow很迷惑。

1
2
3
4
5
java.lang.IllegalStateException:Can not perform thisaction 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的狀態保存之後,嘗試去提交一個FragmentTransaction。這種現象被稱爲活動狀態丟失(Activity State Loss)。然而,在我們瞭解這種異常的真正含義之前,讓我們先看看當onSaveInstanceState()函數被調用的時候到底發生了什麼。

正如最近我在關於Binders & Death Recipients博客裏面討論的那樣,Android應用在Android運行環境裏很難決定自己的命運。Android系統可以在任何時候通過結束一個進程以釋放內存,而且background activities可能在沒有任何警告的情況下被清理。爲了確保這種不確定的行爲對於用戶是透明的,在Activity可以銷燬之前,通過調用onSaveInstanceState()方法,架構給每個Activity一個保存自身狀態的機會。在重新加載已保存的狀態時,對於foreground和background Activities的切換,爲用戶帶來了無縫切換的體驗。用戶不用去關心這個Activity是否被系統銷燬了。

在框架調用onSaveInstanceState()方法時,給這個方法傳遞了一個Bundle對象。Activity可以通過這個對象來存儲它的狀態,而且Activity把它的dialogs、fragments以及views的狀態都保存在這個對象裏面。當這個函數返回時,系統打包這個Bundle對象通過一個Binder接口傳遞給系統服務處理,然後它會被安全的存儲下來。當系統決定重新創建這個Activity的時候,它會給這個應用傳回一個相同的Bundle對象,通過這個對象可以重新裝載Activity銷燬時的狀態。

那爲什麼會拋出這個異常呢?這個問題源於這樣的事實,Bundle對象代表一個Activity在調用onSaveInstanceState()方法的一個瞬間快照,僅此而已。這意味着,當你在onSaveInstanceState()方法調用後會調用FragmentTransaction的commit方法。這個transaction將不會被記住,因爲它沒有在第一時間記錄爲這個Activity的狀態的一部分。從用戶的角度來看,這個transaction將會丟失,可能導致UI狀態丟失。爲了保證用戶的體驗,Android不惜一切代價避免狀態的丟失。因此,無論什麼時候發生,都將簡單的拋出一個IllegalStateException異常。

什麼時候會拋出這個異常?

如果之前你遇到過這個異常,也許你已經注意到異常拋出的時間在不同的版本平臺有細微的差別。也許你會發現,老版本的機器拋出異常的頻率更低,或者你的應用使用Support Library比使用官方的框架類的時候更容易拋出異常。這個細微的區別已經導致一些人在猜測Support Library有bug,是不值得相信的。然而,這樣的猜想完全錯誤。

這些細微區別存在的原因是源於Honeycomb上對於Activity生命週期所做的巨大改變。在Honeycomb之前,Activity直到暫停後才考慮被銷燬。這意味着在onPause()方法之前onSaveInstanceState()方法被立即調用。然而,從Honeycomb開始,考慮銷燬Activity只能是在他們停止之後,這意味着onSaveInstanceState()方法現在是在onStop()方法之前調用,以此代替在onPause()方法之前調用。這些不同總結如下表:

  Honeycomb之前的版本 Honeycomb及更新的版本
Activities會在onPause()調用前被結束? NO NO
Activities會在onStop()調用前被結束? YES NO
onSaveInstanceState(Bundle)會在哪些方法調用前被執行? onPause() onStop()

作爲Activity生命週期已做的細微改變的結果,Support Library有時候需要根據平臺的版本來改變它的行爲。比如,在Honeycomb及以上的設備中,每當一個commit方法在onSaveInstanceState()方法之後調用時,都會拋出一個異常來提醒開發者狀態丟失發生了。然而,在Honeycomb之前的設備上,每次它發生時並拋出異常將更受限制,他們的onSaveInstanceState()方法在Activity的生命週期中更早調用,結果更容易發生狀態丟失。Android團隊被迫做了一個折中的辦法:爲了更好的與老版本平臺交互,老的設備不得不接受偶然狀態丟失可能發生在onPause()方法和onStop()方法之間。Support Library在不同平臺的行爲總結如下表:

  Honeycomb之前的版本 Honeycomb及更新的版本
commit()在onPause()前被調用 OK OK
commit()在onPause()和onStop()執行中間被調用 STATE LOSS OK
commit()在onStop()之後被調用 EXCEPTION EXCEPTION

如何避免拋出異常?

一旦你瞭解了到底發生了什麼,避免發生Activity狀態丟失將會很簡單。如果你讀了這篇博客,那麼很幸運你更好的瞭解了Support Library是怎麼工作的,以及在你的應用中避免狀態丟失爲什麼如此的重要。假如你查看這個博客是爲了查找快速解決的辦法,那麼,當你在你的應用中使用FragmentTransactions的時候,應牢記以下的這些建議:

建議一

當你在Activity生命週期函數裏面提交transactions的時候要小心。大部分的應用僅僅在onCreate()方法被調用的開始時間提交transactions,或者在相應用戶輸入的時候,因此將不可能碰到任何問題。然而,當你的transactions在其他的Activity生命週期函數提交,如onActivityResult()onStart()onResume(),事情將會變得微妙。例如,你不應該在FragmentActivity的onResume()方法中提交transactions。因爲有些時候這個函數可以在Activity的狀態恢復前被調用(可以查看相關文檔瞭解更多信息)。如果你的應用要求在除onCreate()函數之外的其他Activity生命週期函數中提交transaction,你可以在FragmentActivity的onResumeFragments()函數或者Activity的onPostResume()函數中提交。這兩個函數確保在Activity恢復到原始狀態之後纔會被調用,從而避免了狀態丟失的可能性。(示例:看看我對this StackOverflow question的回答,來想想如何提交FragmentTransactions作爲Activity的onActivityResult方法被調用的響應)。

建議二

避免在異步回調函數中提交transactions。包括常用的方法,比如AsyncTask的onPostExecute方法和LoaderManager.LoaderCallbacks的onLoadFinished方法。在這些方法中執行transactions的問題是,當他們被調用的時候,他們完全沒有Activity生命週期的當前狀態。例如,考慮下面的事件序列:

  1. 一個Activity執行一個AsyncTask。
  2. 用戶按下“Home”鍵,導致Activity的onSaveInstanceState()onStop()方法被調用。
  3. AsyncTask完成並且onPostExecute方法被調用,而它沒有意識到Activity已經結束了。
  4. 在onPostExecute函數中提交的FragmentTransaction,導致拋出一個異常。

一般來說,避免這種類型異常的最好辦法就是不要在異步回調函數中提交transactions。Google工程師似乎同意這個信條。根據Android Developers group上的這篇文章,Android團隊認爲UI主要的改變,源於從異步回調函數提交FragmentTransactions引起不好的用戶體驗。如果你的應用需要在這些回調函數中執行transaction而沒有簡單的方法可以確保這個回調函數不好在onSaveInstanceState()之後調用。你可能需要訴諸於使用commitAllowingStateLoss方法,並且處理可能發生的狀態丟失。(可以看看StackOverflow上的另外兩篇文章,這一篇另一篇)。

建議三

作爲最後的辦法,使用commitAllowingStateLoss()函數。commit()函數和commitAllowingStateLoss()函數的唯一區別就是當發生狀態丟失的時候,後者不會拋出一個異常。通常你不應該使用這個函數,因爲它意味可能發生狀態丟失。當然,更好的解決方案是commit函數確保在Activity的狀態保存之前調用,這樣會有一個好的用戶體驗。除非狀態丟失的可能無可避免,否則就不應該使用commitAllowingStateLoss()函數。

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