1. 程式人生 > >Android Fragment使用(三) Activity, Fragment, WebView的狀態儲存和恢復

Android Fragment使用(三) Activity, Fragment, WebView的狀態儲存和恢復

Android中的狀態儲存和恢復

Android中的狀態儲存和恢復, 包括Activity和Fragment以及其中View的狀態處理.
Activity的狀態除了其中的View和Fragment的狀態之外, 還需要使用者手動儲存一些成員變數.
Fragment的狀態有它自己的例項狀態和其中的View狀態, 因為其生命週期的靈活性和實際需要的不同, 情況會多一些.
根據原始碼, 列出了Fragment中例項狀態和View狀態儲存和恢復的幾個入口, 便於分析檢視.
最後專門講了WebView狀態儲存和恢復, 問題及處理.
還有一個工具類icepick的介紹.

Activity的狀態儲存和恢復

作為熱身, 先來講一下Activity的狀態儲存和恢復.

什麼時候需要恢復Activity

關於Activity的銷燬和重建, 之前有這麼一篇博文: Activity的重新建立
總結來說, 就是Activity的銷燬, 分為徹底銷燬和留下資料的銷燬兩種.

徹底銷燬是指使用者主動去關閉或退出這個Activity. 此時是不需要狀態恢復的, 因為下次回來又是重新建立全新的例項.
留下資料的銷燬是指系統銷燬了activity, 但是當用戶返回來時, 會重新建立它, 讓使用者覺得它一直都在.

螢幕旋轉重建可以歸結為第二種情況, 開啟Do not keep activities開關, 切換activities也是會出現第二種情況.
開啟Do not keep activities

開關就是為了模擬記憶體不足時的系統行為, 這裡有一篇分析

如何恢復

實際上系統已經幫我們做好了View層面基本的恢復工作, 主要是依靠下面兩個方法:

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        // 在onStop()之前呼叫, 文件中說並不保證在onPause()的之前還是之後
        // 我的試驗中一般是在onPause()之後
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        // 在onStart() 之後
    }

Bundle其中包含了activity中的view和fragment的各種資訊, 所以呼叫基類的方法就可以完成基本的view層面的恢復工作.
注意這兩個方法並不是activity的生命週期回撥, 對於activity來說它們不是一定會發生的.
另外需要注意的是, View必須要有id才能被恢復.

舉一個例項來說明:
Activity A start B, 那麼A的onSaveInstanceState()會在onStop()之前呼叫, 以防A被系統銷燬.
但是在B中按下back鍵finish()了自己後, B被銷燬的過程中, 並沒有呼叫onSaveInstanceState(), 是因為B並沒有被壓入task的back stack中,
也即系統知道B並不需要儲存自己的狀態.
正常情況下, 返回到A, A沒有被銷燬, 也不會呼叫onRestoreInstanceState(), 因為所有的狀態都還在, 並不需要重建.

如果我們打開了Do not keep activities開關, 模擬系統記憶體不足時的行為, 從A到B, 可以看到當B resume的時候A會一路走到onDestroy(),
而關掉B之後, A會從onCreate()開始走, 此時onCreate()的引數bundle就不為空了, onStart()之後會呼叫onRestoreInstanceState()方法, 其引數bundle中內容類似於如下:

Bundle[{android:viewHierarchyState=Bundle[mParcelledData.dataSize=272]}]

其中包含了View的狀態, 如果有Fragment, 也會包含Fragment的狀態, 其實質是儲存了FragmentManagerState, 內容類似於如下:

Bundle[{android:viewHierarchyState=Bundle[{android:views={[email protected], 2131492950=CompoundButton.SavedState{4034f96 checked=true}, [email protected]}}], android:[email protected]}]

對於上面的例子來說, B什麼時候會呼叫onSaveInstanceState()呢?
當從A開啟B之後, 按下Home鍵, B就會呼叫onSaveInstanceState().
因為這時候系統不知道使用者什麼時候會返回, 有可能會把B也銷燬了, 所以儲存一下它的狀態.
如果下次回來它沒有被重建, onRestoreInstanceState()就不會被呼叫, 如果它被重建了, onRestoreInstanceState()才會被呼叫.

Activity儲存方法的呼叫時機

activity的onSaveInstanceState()onRestoreInstanceState()方法在如下情形下會呼叫:

  1. 螢幕旋轉重建: 先save再restore.
  2. 啟動另一個activity: 當前activity在離開前會save, 返回時如果因為被系統殺死需要重建, 則會從onCreate()重新開始生命週期, 呼叫onRestoreInstanceState(); 如果沒有重建, 則不會呼叫onCreate(), 也不會呼叫onRestoreInstanceState(), 生命週期從onRestart()開始, 接著onStart()和onResume().
  3. 按Home鍵的情形和啟動另一個activity一樣, 當前activity在離開前會save, 使用者再次點選應用圖示返回時, 如果重建發生, 則會呼叫onCreate()和onRestoreInstanceState(); 如果activity不需要重建, 只是onRestart(), 則不會呼叫onRestoreInstanceState().

Activity恢復方法的呼叫時機

activity的onSaveInstanceState()onRestoreInstanceState()方法在如下情形下不會呼叫:

  1. 使用者主動finish()掉的activity不會呼叫onSaveInstanceState(), 包括主動按back退出的情況.
  2. 新建的activity, 從onCreate()開始, 不會呼叫onRestoreInstanceState().

Activity中還需要手動恢復什麼

如上, 系統已經為我們恢復了activity中的各種view和fragment, 那麼我們自己需要儲存和恢復一些什麼呢?
答案是成員變數值.

因為系統並不知道你的各種成員變數有什麼用, 哪些值需要儲存, 所以需要你自己覆寫上面兩個方法, 然後把自己需要儲存的值加進bundle裡面去. 具體例子, 這裡Activity的重新建立有, 我就不重複了.
重要的是不要忘記呼叫super的方法, 那裡有系統幫我們恢復的工作.

工具類Icepick介紹

在介紹下面的內容之前, 先介紹一個小工具: Icepick
這個工具的作用是, 在你想儲存和重建自己的成員變數資料時, 幫你省去那些put和get方法的呼叫, 你也不用為每一個欄位起一個常量key.
你需要做的就是簡單地在你想要儲存狀態的欄位上面加上一個@State 註解.
然後在儲存和恢復的時候分別加上一句話:

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Icepick.restoreInstanceState(this, savedInstanceState);
  }

  @Override
  public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Icepick.saveInstanceState(this, outState);
  }

然後你的成員變數就有了它應該有的值了, DONE!

Fragment的狀態儲存和恢復

Fragment的狀態比Activity的要複雜一些, 因為它的生命週期狀態比較多.

Fragment狀態儲存和恢復的相關方法

按照上面的思路, 我先去查詢Fragment中儲存和恢復的回撥方法了.
Fragment的狀態儲存回撥是這個方法:

    public void onSaveInstanceState(Bundle outState) {
        // may be called any time before onDestroy()
    }

這個方法和之前activity的情況大體是類似的, 它不是生命週期的回撥, 所以只在有需要的時候會調到.
onSaveInstanceState()在activity呼叫onSaveInstanceState()的時候發生, 用於儲存例項狀態.(看它的方法名: instance state).
onSaveInstanceState()方法儲存的bundle會返回給幾個生命週期回撥: onCreate(), onCreateView(), onViewCreated()onActivityCreated().

Fragment並沒有對應的onRestoreInstanceState()方法.
也即沒有例項狀態的恢復回撥.

Fragment只有一個onViewStateRestored()的回撥方法:

    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        // 在onActivityCreated()和onStart()之間呼叫
        mCalled = true;
    }

onViewStateRestored()每次新建Fragment都會發生.
它並不是例項狀態恢復的方法, 只是一個View狀態恢復的回撥.

在這裡我先上個結論, 把檢視原始碼中Fragment狀態儲存和恢復的相關方法列出來:

Fragment狀態儲存入口:
Fragment狀態儲存

Fragment的狀態儲存入口有三個:

  1. Activity的狀態儲存, 在Activity的onSaveInstanceState()裡, 呼叫了FragmentManger的saveAllState()方法, 其中會對mActive中各個Fragment的例項狀態和View狀態分別進行儲存.
  2. FragmentManager還提供了public方法: saveFragmentInstanceState(), 可以對單個Fragment進行狀態儲存, 這是提供給我們用的, 後面會有例子介紹這個. 其中呼叫的saveFragmentBasicState()方法即為情況一中所用, 圖中已畫出標記.
  3. FragmentManager的moveToState()方法中, 當狀態回退到ACTIVITY_CREATED, 會呼叫saveFragmentViewState()方法, 儲存View的狀態.

moveToState()方法中有很長的switch case, 中間不帶break, 基本是根據新狀態和當前狀態的比較, 分為正向建立和反向銷燬兩個方向, 一路沿著多個case走下去.

Fragment狀態恢復入口:
Fragment狀態恢復

三個恢復的入口和三個儲存的入口剛好對應.

  1. 在Activity重新建立的時候, 恢復所有的Fragment狀態.
  2. 如果呼叫了FragmentManager的方法: saveFragmentInstanceState(), 返回值得到的狀態可以用Fragment的setInitialSavedState()方法設定給新的Fragment例項, 作為初始狀態.
  3. FragmentManager的moveToState()方法中, 當狀態正向建立到CREATED時, Fragment自己會恢復View的狀態.

這三個入口分別對應的情況是:
入口1對應系統銷燬和重建新例項.
入口2對應使用者自定義銷燬和建立新Fragment例項的狀態傳遞.
入口3對應同一Fragment例項自身的View狀態重建.

Fragment狀態儲存恢復和Activity的聯絡

這裡對應的是入口1的情況.
當Activity在做狀態儲存和恢復的時候, 在它其中的fragment自然也需要做狀態儲存和恢復.
所以Fragment的onSaveInstanceState()在activity呼叫onSaveInstanceState()的時候一定會發生.
同樣的, 如果Fragment中有一些成員變數的值在此時需要儲存, 也可以用@State標記, 處理方法和上面一樣.
也即, 在Activity需要儲存狀態的時候, 其中的Fragments的例項狀態自動被處理儲存.

Fragment同一例項的View狀態恢復

這裡對應的是入口3的情況.
前面介紹過, activity在儲存狀態的時候, 會將所有View和Fragment的狀態都儲存起來等待重建的時候使用.
但是如果是單個Activity對應多個Fragments的架構, Activity永遠是resume狀態, 多個Fragments在切換的過程中, 沒有activity的幫助, 如何儲存自己的狀態?

首先, 取決於你的多個Fragments是如何初始化的.
我做了一個實驗, 在activity的onCreate()裡面初始化兩個Fragment:

private void initFragments() {
    tab1Fragment = getFragmentManager().findFragmentByTag(Tab1Fragment.TAG);
    if (tab1Fragment == null) {
        tab1Fragment = new Tab1Fragment();
    }
    tab2Fragment = getFragmentManager().findFragmentByTag(Tab2Fragment.TAG);
    if (tab2Fragment == null) {
        tab2Fragment = new Tab2Fragment();
    }
}

然後點選兩個按鈕來切換它們, replace(), 並且不加入到back stack中:

@OnClick(R.id.tab1)
void onTab1Clicked() {
    getFragmentManager().beginTransaction()
            .replace(R.id.content_container, tab1Fragment, Tab1Fragment.TAG)
            .commit();
}

@OnClick(R.id.tab2)
void onTab2Clicked() {
    getFragmentManager().beginTransaction()
            .replace(R.id.content_container, tab2Fragment, Tab2Fragment.TAG)
            .commit();

}

可以看到, 每一次的切換, 都是一個Fragment的完全destroy, detach和另一個fragment的attach, create,
但是當我在這兩個fragment中各自加上EditText, 發現只要EditText有id, 切換過程中EditText的內容是被儲存的.
這是誰在什麼時候儲存並恢復的呢?
我在TextChange的回撥裡打了斷點, 發現呼叫棧如下:
Fragment restore view
FragmentManagerImpl中, moveToState()方法的case Fragment.CREATED中:
呼叫了: f.restoreViewState(f.mSavedFragmentState);
此時我沒有做任何儲存狀態的處理, 但是斷點中可以看出:
Fragment states
雖然mSavedFragmentState是null, 但是mSavedViewState卻有值.
所以這個View狀態儲存和恢復對應的入口即是上面兩個圖中的入口三.

這是因為我的兩個fragment只new了一次, 然後儲存了成員變數, 即便是Fragment重新onCreate(), 但是對應的例項仍然是同一個.
這和Activity是不同的, 因為你是無法new一個Activity的.

在上面的例子中, 如果不儲存Fragment的引用, 每次都new Fragment, 那麼View的狀態是不會被儲存的, 因為不同例項間的狀態傳遞只有在系統銷燬恢復的情況下才會發生(入口一).
如果我們需要在不同的例項間傳遞狀態, 就需要用到下面的方法.

不同Fragment例項間的狀態儲存和恢復

這裡對應的是入口2, 不同於入口1和3, 它們是自動的, 入口2是使用者主動儲存和恢復的情形.
自己主動儲存Fragment的狀態, 可以呼叫FragmentManager的這個方法:

public abstract Fragment.SavedState saveFragmentInstanceState(Fragment f);

它的實現是這樣的:

@Override
public Fragment.SavedState saveFragmentInstanceState(Fragment fragment) {
    if (fragment.mIndex < 0) {
        throwException(new IllegalStateException("Fragment " + fragment
                + " is not currently in the FragmentManager"));
    }
    if (fragment.mState > Fragment.INITIALIZING) {
        Bundle result = saveFragmentBasicState(fragment);
        return result != null ? new Fragment.SavedState(result) : null;
    }
    return null;
}

返回的資料型別是: Fragment.SavedState, 這個state可以通過Fragment的這個方法設定給自己:

public void setInitialSavedState(SavedState state) {
    if (mIndex >= 0) {
        throw new IllegalStateException("Fragment already active");
    }
    mSavedFragmentState = state != null && state.mState != null
            ? state.mState : null;
}

但是注意只能在Fragment被加入之前設定, 這是一個初始狀態.
利用這兩個方法可以更加自由地儲存和恢復狀態, 而不依賴於Activity.
這樣處理以後, 不必儲存Fragment的引用, 每次切換的時候雖然都new了新的例項, 但是舊的例項的狀態可以設定給新例項.

例子程式碼:

@State
SparseArray<Fragment.SavedState> savedStateSparseArray = new SparseArray<>();

void onTab1Clicked() {
    // save current tab
    Fragment tab2Fragment = getSupportFragmentManager().findFragmentByTag(Tab2Fragment.TAG);
    if (tab2Fragment != null) {
        saveFragmentState(1, tab2Fragment);
    }

    // restore last state
    Tab1Fragment tab1Fragment = new Tab1Fragment();
    restoreFragmentState(0, tab1Fragment);

    // show new tab
    getSupportFragmentManager().beginTransaction()
            .replace(R.id.content_container, tab1Fragment, Tab1Fragment.TAG)
            .commit();
}

private void saveFragmentState(int index, Fragment fragment) {
    Fragment.SavedState savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
    savedStateSparseArray.put(index, savedState);
}

private void restoreFragmentState(int index, Fragment fragment) {
    Fragment.SavedState savedState = savedStateSparseArray.get(index);
    fragment.setInitialSavedState(savedState);
}

注意這裡用了SparseArray來儲存Fragment的狀態, 並且加上了@State, 這樣在Activity重建的時候其中的內容也能夠被恢復.

Back stack中的fragment

有一點很特殊的是, 當Fragment從back stack中返回, 實際上是經歷了一次View的銷燬和重建, 但是它本身並沒有被重建.
即View狀態需要重建, 例項狀態不需要重建.

舉個例子說明這種情形: Fragment被另一個Fragment replace(), 並且壓入back stack中, 此時它的View是被銷燬的, 但是它本身並沒有被銷燬.
也即, 它走到了onDestroyView(), 卻沒有走onDestroy()onDetact().
等back回來的時候, 它的view會被重建, 重新從onCreateView()開始走生命週期.
在這整個過程中, 該Fragment中的成員變數是保持不變的, 只有View會被重新建立.
在這個過程中, instance state的saving並沒有發生.

所以, 很多時候Fragment還需要考慮的是在沒有Activity幫助的情形下(Activity並沒有可能重建的情形), 自身View狀態的儲存.
此時要注意一些不容易發現的錯誤, 比如List的新例項需要重新setAdapter等.

Fragment setRetainInstance

Fragment有一個相關方法:
setRetainInstance
這個方法設定為true的時候表示, 即便activity重建了, 但是fragment的例項並不被重建.
注意此方法只對沒有放在back stack中的fragment生效.
什麼時候要用這個方法呢? 處理configuration change的時候:
Handling Configuration Changes with Fragments
這樣, 當螢幕旋轉, Activity重建, 但是其中的fragment和fragment正在執行的任務不必重建.
更多解釋可以參見:
http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean
http://stackoverflow.com/questions/11160412/why-use-fragmentsetretaininstanceboolean

注意這個方法只是針對configuration change, 並不影響使用者主動關閉和系統銷燬的情況:
當activity被使用者主動finish, 其中的所有fragments仍然會被銷燬.
當activity不在最頂端, memory不夠了, 系統仍然可能會銷燬activity和其中的fragments.

View的狀態儲存和恢復

View的狀態儲存和恢復主要是依賴於下面幾個方法:
儲存: saveHierarchyState() -> dispatchSaveInstanceState() -> onSaveInstanceState()
恢復: restoreHierarchyState() -> dispatchRestoreInstanceState() -> onRestoreInstanceState()
還有兩個重要的前提條件是View要有id, 並且setSavedEnabled()為true.(這個值預設為true).
在系統的widget裡(比如TextView, EditText, Checkbox等), 這些都是已經被處理好的, 我們只需要給View賦予id, Activity和Fragment重建的時候會自動恢復其中的狀態. (這裡的Fragment恢復對應入口一和入口三, 入口二屬於跨例項新建的情況).

但是如果你要使用第三方的自定義View, 就需要確認一下它們內部是否有狀態儲存和恢復的程式碼.
如果不行你就需要繼承該自定義View, 然後實現這兩個方法:

//
// Assumes that SomeSmartButton is a 3rd Party view that
// View State Saving/Restoring are not implemented internally
//
public class SomeBetterSmartButton extends SomeSmartButton {

    ...

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        // Save current View's state here
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        // Restore View's state here
    }

    ...

}

WebView的狀態儲存和恢復

WebView的狀態儲存和恢復不像其他原生View一樣是自動完成的.
WebView不是繼承自View的.
如果我們把WebView放在佈局裡, 不加處理, 那麼Activity或Fragment重建的過程中, WebView的狀態就會丟失, 變成初始狀態.

在Fragment的onSaveInstanceState()裡面可以加入如下程式碼來儲存WebView的狀態:

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    webView.saveState(outState);
}

然後在初始化的時候, 增加判斷, 不必每次都開啟初始連結:

if (savedInstanceState != null) {
    webView.restoreState(savedInstanceState);
} else {
    webView.loadUrl(TEST_URL);
}

這樣處理以後, 在重新建立的時候, WebView的狀態就能恢復到離開前的頁面.
不論WebView是放在Activity裡還是Fragment裡, 這個方法都適用.

但是Fragment還有另一種情況, 即Fragment被壓入back stack, 此時它沒有被destroy(), 所以沒有呼叫onSavedInstanceState()這個方法.
這種情況返回的時候, 會從onCreateView()開始, 並且savedInstanceState為null, 於是其中WebView之前的狀態在此時丟失了.
解決這種情況可以利用Fragment例項並未銷燬的條件, 增加一個成員變數bundle, 儲存WebView的狀態, 最終解決如下:

private Bundle webViewState;

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    ButterKnife.bind(this, view);

    initWebView();
    if (webViewState != null) {
        //Fragment例項並未被銷燬, 重新create view
        webView.restoreState(webViewState);
    } else if (savedInstanceState != null) {
        //Fragment例項被銷燬重建
        webView.restoreState(savedInstanceState);
    } else {
        //全新Fragment
        webView.loadUrl(TEST_URL);
    }
}

@Override
public void onPause() {
    super.onPause();
    webView.onPause();

    //Fragment不被銷燬(Fragment被加入back stack)的情況下, 依靠Fragment中的成員變數儲存WebView狀態
    webViewState = new Bundle();
    webView.saveState(webViewState);
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    //Fragment被銷燬的情況, 依靠outState儲存WebView狀態
    if (webView != null) {
        webView.saveState(outState);
    }
}

參考資料