Fragment系列之Transactions.commit和Activity的狀態

關於java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState出現原因與解決方法的幾點總結

問題描述:我們在使用Fragment時常會碰見的一個錯誤,就是在調用Transactions.commit後,會收到一個java.lang.IllegalStateException的錯誤。比如這樣的:

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)

我有理由相信很多人在剛開始使用Fragment是都幾乎會遇見這樣類似的錯誤,那麼這篇博文就這個問題出現的原因以及解決的幾個方法作一個總結。

爲什麼會出現這個異常?

其它我們可以在這個異常的說明上,看出一點端倪。 大概意思是:讓我們嘗試 commit() 一個Transactions時,要檢查Activity的狀態,也就是說我們不能在Activity的狀態保存後去提交一個Transactions。我們通常稱這種現象爲Activity State Loss (活動狀態丟失)。那麼這個與 onSaveInstanceState() 有什麼關係呢?

當我們談論onSaveInstanceState() 時,我們在談論什麼?

這個函數執行時,Activity到底做了什麼?我們都知道每個Activity都有自己的生命週期,都會執行自己的生命週期函數。這個是Android架構決定的。
我們知道,當Activity調用這個方法時,會傳一個Bundle對象,那麼這個對象就是用來 保存Activity的各個狀態的。包括了屬於它的Dialog、Fragment以及各個Views的狀態。當然這些都是系統自己處理的。當系統重建這個Activity時,就可以還原銷燬之前保存的各個狀態。
既然如此,那怎麼還會出現這個異常呢?Android給出瞭解釋:

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

何時會出現這個異常?

或許你在遇到這個問題的時候不盡相同,那麼有些人就會自己臆斷說:其實這是Support Library自身的bug,或許是,或許也不是。確切地說其實是與平臺相關,不現版本的平臺有細微的差別。這些細微區別存在的原因是源於Honeycomb上對於Activity生命週期所做的巨大改變。

在Honeycomb之前,Activity直到暫停後才考慮被銷燬。這意味着在onPause()方法之前onSaveInstanceState()方法被立即調用。然而,從Honeycomb開始,考慮銷燬Activity只能是在他們停止之後,這意味着onSaveInstanceState()方法現在是在onStop()方法之前調用,以此代替在onPause()方法之前調用。

這些不同總結如下表:

說明 Honeycomb之前的版本 Honeycomb及更新的版本
Activity在onPause()調用前結束 N N
Activity在onStop()調用前結束 Y N
onSaveInstanceState()執行時機 onPause() onStop()

由於不同版本的Activity生命週期管理存在細微的差別,那麼Support Library在不同版本上的實現也就會隨着系統版本的變動而做出相應的變動。比如,在Honeycomb及以上的設備中,每當commit()onSaveInstanceState()之後執行時,都會拋出一個異常來提醒開發者狀態丟失了。然而,在Honeycomb之前的設備上,它發生並拋出異常將更受限制,因爲他們的onSaveInstanceState()在Activity的生命週期中更早調用,結果更容易發生狀態丟失。 Support Library在不同版本的行爲總結如下表:

說明 Honeycomb之前的版本 Honeycomb及更新的版本
commit()onPause()前調用 OK OK
commit()onPause()onStop()間調用 LOSS OK
commit()onStop()後調用 EXCEPTION EXCEPTION

避免異常出現的幾點建議

俗話說:知其然,知其所以然。當我們真正瞭解了問題的出現的原因後,想必解決問題也就會輕鬆很多。所以以後我們在使用Transactions時,一定要牢記下面的幾點建議:

Tips 1 不要輕易在onCreate()以外的生命週期函數內執行commit()

在Activity的生命週期函數內commit()Transactions時,千萬要小心。當然我們大部分時候是在onCreate()這個週期內使用,這並沒有什麼問題。但是,我人不少會碰見在諸如:如onActivityResult()onStart()onResume(),特別是onActivityResult()commit()的情況,這個時候,當我們認爲很安全的時候,事情往往出乎意料地變得微妙起來。因爲某引起函數可以在Activity的狀態恢復前被調用。這個時候Activity還是處於State Loss的狀態,問題往往又會如同幽靈般的出現了。當然,如果必須要在這些時候調用的,Android也給出了一些妥協的解決辦法:

如果你的應用要求在除onCreate()函數之外的其他Activity生命週期函數中提交transaction,你可以在FragmentActivity的onResumeFragments()函數或者Activity的onPostResume()函數中提交。這兩個函數確保在Activity恢復到原始狀態之後纔會被調用,從而避免了狀態丟失的可能性。

比如上面我提到的Activity的onActivityResult()方法內提交Transactions的需求,我在StackOverflow中找到個一個很好的回答,大家如果有遇到這個問題,可以去看看

Tips 2 避免在異步回調函數中不安全地提交Transactions

比如AsyncTask的onPostExecute()方法和LoaderManager.LoaderCallbacks的onLoadFinished()方法。因爲在這些方法內,完全沒有Activity生命週期的當前狀態。一個有趣的事件序列:

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

所以,避免的方法就是將在源頭上杜絕,永遠不要在不要在異步回調函數中不安全地提交Transactions。當然這也不是我說的,Google的工程師也青睞這種看法。我無意中在Android Developers group上看到一篇博文卻是一個佐證。當然我同樣在StackOverflow上看到了問題1問題2回答,從使用的角度也證明了這一點。

Tips 3 使用commitAllowingStateLoss()函數

作爲最後一個萬不得已的時候,才能使用的方法,與commit()的唯一區別就是當發生狀態丟失的時候,前者不會拋出一個異常。通常來講我們不應該使用這個函數,因爲它很可能會引起狀態丟失。

寫在最後

寫了這麼多,也重新認識了Transactions與Activity狀態之間的微妙關係,到最後來也就是兩點:

  1. commit()函數確保在Activity的狀態保存之前調用
  2. 除非狀態丟失的可能無可避免,否則就不應該使用commitAllowingStateLoss()函數。

鳴謝

Mr.Dogy的生活意見的微博
Mr.Dogy的生活意見的微信

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章