讓你不再俱怕Fragment State Loss

1114288530 8年前發布 | 7K 次閱讀 安卓開發 Android開發 移動開發

使用過 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/

 

 本文由用戶 1114288530 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!