讓你不再俱怕Fragment State Loss
使用過 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那樣健壯好用。作為我們開發者,要么就干脆不用它,要么就把它研究透徹再使用,否則將會陷入無盡痛苦之中。
參考資料
來自:http://toughcoder.net/blog/2016/11/28/fear-android-fragment-state-loss-no-more/