讓你不再俱怕Fragment State Loss

讓你不再俱怕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那樣健壯好用。作爲我們開發者,要麼就乾脆不用它,要麼就把它研究透徹再使用,否則將會陷入無盡痛苦之中。

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