Fragment Transactions和Activity狀態丟失

獨孤昊天發表於2014-08-19

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

這篇部落格將會解釋,這個異常在什麼時候發生以及為什麼會發生?並且提供幾種方法讓這種異常不會發生在你的應用中。

為什麼會丟擲這個異常?

這種異常的出現是由於,在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()函式。

相關文章