讓你不再俱怕Fragment State Loss
原文鏈接http://toughcoder.net/blog/2016/11/28/fear-android-fragment-state-loss-no-more/
使用過Fragment的人我相信對臭名昭著的狀態丟失問題(IllegalStateException: Can not perform this action after onSaveInstanceState)一定不會陌生。曾經被這個問題困擾了很久,相信很多同學也是。花些時間來好好把它研究一下,以弄懂爲何會有這樣的問題產生,然後就可以解決問題,或者合理的規避問題。
什麼是狀態恢復
安卓的狀態恢復是一個比較令人困惑的特性,它源於拙劣的系統設計。當一個頁面正在顯示的時候,如果系統發生了一些變化,變化又足以影響頁面的展示,比如屏幕旋轉了,語言發生了變化等。安卓的處理方式就是把頁面(Activity)強制殺掉,再重新創建它,重建時就可以讀取到新的配置了。又或者,當離開了一個頁面,再回到頁面時,如果頁面(Activity)因爲資源不足被回收了,那麼當再回到它時,系統也會重新創建這個頁面。
狀態恢復,是爲了保持更好的用戶體驗,讓用戶感覺認爲頁面,是一直存在的,類似於處理器調用函數的保護現場和恢復現場。
Activity有二個鉤子onSaveInstanceState和onRestoreInstanceState就是用來保存狀態和恢復狀態的。
當從Honeycomb引入了Fragment後,爲了想讓開發者更多的使用Fragment,或者想讓Fragment更容易的使用,狀態保存與恢復的時候也必須要把Fragment保存與恢復。Fragment本質上就是一個View tree,強行附加上一些生命週期鉤子。所以,爲了讓頁面能恢復成先前的樣子,View是必須要重新創建的,因此Fragment是必須要恢復的。
Fragment的作用域是Activity,FragmentManager管理着一個Activity所有的Fragment,這些Fragment被放入一個棧中。每個Fragment有一個FragmentState,它相當於Fragment的snapshot,保存狀態時FragmentManager把每個Fragment的FragmentState存儲起來,最終存儲到Activity的savedInstanceState中。
爲什麼會有這個異常
既然狀態的保存與恢復都必須要把Fragment帶上,那麼一旦當Fragment的狀態已保存過了,那麼就不應該再改變Fragment的狀態。因此FragmentManager的每一個操作前,都會調用一個方法來檢查狀態是否保存過了:
private void checkStateLoss() {
if (mStateSaved) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
if (mNoTransactionsBecause != null) {
throw new IllegalStateException(
"Can not perform this action inside of " + mNoTransactionsBecause);
}
}
Fragment狀態保存是在Activity#onSaveInstanceState時做的,會調用FragmentManager#saveAllState方法,來進行Fragment的狀態保存,同時設置mStateSaved爲true,以標識狀態已被保存過。
發生的場景以及如何應對
FragmentTransaction#commit()
棧信息是這樣子的:
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)
或者是這樣的:
java.lang.IllegalStateException: Activity has been destroyed
at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1456)
at android.app.BackStackRecord.commitInternal(BackStackRecord.java:707)
at android.app.BackStackRecord.commit(BackStackRecord.java:671)
at net.toughcoder.miscellaneous.FragmentTestActivity
原因就是commit操作發生在了狀態保存之後。Activity#onSaveInstanceState的調用是不受開發者控制的,並且不同的安卓版本之間存在差異。具體的可以參考大神的文章。
解決之道,如大神提的一樣,就是保證Fragment的操作發生在Activity可見週期之內,換句話說,Fragment的操作應該發生在Activity#onResume與Activity#onPause之間,爲什麼限制這麼死呢?一方面爲了防止上面問題發生;另外,Fragment本質上是View,View的操作理應該是頁面處於活動狀態時才應該進行。
關鍵的點就是小心控制異步任務,在onPause或者最遲在onStop中要終止所有的異步任務。
另外,大招就是使用commitAllowStateLoss。
Activity#onBackPressed
還有一種情況,也會出現此異常,而且是在Activity中完全沒有Fragment的情況下:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1434) at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:577) at android.app.Activity.onBackPressed(Activity.java:2751) at net.toughcoder.miscellaneous.FragmentStateLossActivity.onBackPressed(FragmentStateLossActivity.java:90) at net.toughcoder.miscellaneous.FragmentStateLossActivity$1.run(FragmentStateLossActivity.java:59) at android.os.Handler.handleCallback(Handler.java:751) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6077) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)
或者是這樣的:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1500) at android.support.v4.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:584) at android.support.v4.app.FragmentActivity.onBackPressed(FragmentActivity.java:169) at net.toughcoder.miscellaneous.FragmentStateLossActivity.onBackPressed(FragmentStateLossActivity.java:90) at net.toughcoder.miscellaneous.FragmentStateLossActivity$1.run(FragmentStateLossActivity.java:59) at android.os.Handler.handleCallback(Handler.java:751) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6077) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)
這二個異常都是發生在沒有使用Fragment的Activity中。相當的詭異,根本沒有用Fragment爲何還會拋出State loss的異常。只能從棧信息中的方法開始分析:
Activity的onBackPressed:
public void onBackPressed() {
if (mActionBar != null && mActionBar.collapseActionView()) {
return;
}
if (!mFragments.popBackStackImmediate()) {
finishAfterTransition();
}
}
以及FragmentActivity的onBackPressed:
public void onBackPressed() {
if (!mFragments.popBackStackImmediate()) {
supportFinishAfterTransition();
}
}
從其源碼中不難看出,響應BACK鍵時,一定會去pop fragment。前面提到過,FragmentManager在改變Fragment的狀態前(增加,移除,改變生命週期狀態都是改變狀態)都會檢查state loss:
@Override
public boolean popBackStackImmediate() {
checkStateLoss();
executePendingTransactions();
return popBackStackState(mActivity.mHandler, null, -1, 0);
}
前面說了,checkStateLoss其實就是檢查mStateSaved這個變量是否爲true。那麼都哪裏給它設置爲true了呢?對於正統的Activity和Fragment(android.app.*),是在onSaveInstanceState時,且只有這時才設置:
Parcelable saveAllState() {
// Make sure all pending operations have now been executed to get
// our state update-to-date.
execPendingActions();
mStateSaved = true;
// other codes.
}
但是對於support包中的Fragment(android.support.v4.app.*)除了在onSaveInstanceState中設置以外,在onStop中也把mStateSaved置爲true:
public void dispatchStop() {
// See saveAllState() for the explanation of this. We do this for
// all platform versions, to keep our behavior more consistent between
// them.
mStateSaved = true;
moveToState(Fragment.STOPPED, false);
}
所以,無論你用的是哪個Fragment,如果onBackPressed發生在onSavedInstanceState之後,那麼就會上面的crash。 Stack Overflow上面有類似的討論,比較全面和票數較高就是這個和這個。
二個討論中,針對此場景的獲得最多贊同的解法是,覆寫Activity的onSaveInstanceState,然後不要調用super:
@Override
public void onSaveInstanceState() {
// DO NOT call super
}
從上面的分析來看,這個對於android.app.*中的Fragment是能解決問題的,因爲是在Activity的onSaveInstanceState(super.onSaveInstanceState)中才把mStateSaved置爲true,所以不調super,它就仍是false,當再pop時,也就不會拋出異常的。
但是這明顯是一個拙劣的workaround,首先,你在防止系統保存fragment的狀態,可能會引發一引起其他的問題;再有就是,對於support包,這還是不管用,你仍然能夠遇到state loss exception,因爲在其onStop時也會把mStateSaved置爲true。
上面分析得出,問題產生的原因是onBackPressed發生在了onSavedInstance之後,那麼的解法是,同樣設置一個標誌,如果狀態已保存過,就不要再處理onBackPressed:
public class FragmentStateLossActivity extends Activity {
private static final String TAG = "Fragment state loss";
private boolean mStateSaved;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment_state_loss);
mStateSaved = false;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// Not call super won't help us, still get crash
super.onSaveInstanceState(outState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mStateSaved = true;
}
}
@Override
protected void onResume() {
super.onResume();
mStateSaved = false;
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
mStateSaved = true;
}
@Override
protected void onStart() {
super.onStart();
mStateSaved = false;
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (!mStateSaved) {
return super.onKeyDown(keyCode, event);
} else {
// State already saved, so ignore the event
return true;
}
}
@Override
public void onBackPressed() {
if (!mStateSaved) {
super.onBackPressed();
}
}
}
爲了更徹底的杜絕問題,應該是狀態保存過後,都不應該處理KEY事件。
其實,這也是合理的,onBackPressed一般是由BACK觸發的,與KEY事件一樣,都屬於用戶交互事件,用戶交互事件都應該在Activity處於活動期間來響應,特別是過了onStop以後,再處理這樣的事件也是沒有意義的。
通常情況下,是不會發生這樣的問題的,因爲一般情況下是由BACK鍵觸發onBackPressed,onBackPressed中調用finish(),finish纔會觸發銷燬生命週期(save instance,pause,stop,destroy),自然不會產生onBackPressed發生在它們之後,也就沒有此異常。但假如,有人爲處理BACK事件,或者涉及Webview的BACK處理時,就有可能異步處理BACK,從而產生這個異常。
其實,從根兒上來講,這是Android的設計不完善導致的,再看下pop back的實現:
@Override
public boolean popBackStackImmediate() {
checkStateLoss();
executePendingTransactions();
return popBackStackState(mActivity.mHandler, null, -1, 0);
}
難道第一句不應該是先判斷此棧是否爲空嗎?如果爲空(壓根兒就沒有用Fragment),爲什麼要check state loss,爲什麼還要去executePendingTransactions()? 但是,它又不得不這樣做,因爲Fragment的很多操作是異步的,到這個時候,有可能某些Fragment已被用戶commit,但是還沒有真正的添加到stack中去,因爲只有把所有的pending transactions執行完了,才能知道到底有沒有Fragment,但是執行pending transactions就會改變fragment的狀態,就必須要check state loss。
看來萬惡之源就是Fragment的transactions都是異步的。Anyway,Fragment的設計是有很多缺陷的,因爲這並不是系統設計之初就考慮到的東西,所以,不可能像水果裏的ViewController那樣健壯好用。作爲我們開發者,要麼就乾脆不用它,要麼就把它研究透徹再使用,否則將會陷入無盡痛苦之中。