天天看點

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

Android中的狀态儲存和恢複, 包括Activity和Fragment以及其中View的狀态處理.

Activity的狀态除了其中的View和Fragment的狀态之外, 還需要使用者手動儲存一些成員變量.

Fragment的狀态有它自己的執行個體狀态和其中的View狀态, 因為其生命周期的靈活性和實際需要的不同, 情況會多一些.

根據源碼, 列出了Fragment中執行個體狀态和View狀态儲存和恢複的幾個入口, 便于分析檢視.

最後專門講了WebView狀态儲存和恢複, 問題及處理.

還有一個工具類icepick的介紹.

Android中的狀态儲存和恢複

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={16908290=android.view.AbsSavedState$1@bc382e7, 2131492950=CompoundButton.SavedState{4034f96 checked=true}, 2131492951=android.view.AbsSavedState$1@bc382e7}}], android:fragments=android.app.FragmentManagerState@bacc717}]
           

對于上面的例子來說, 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恢複方法的調用時機

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的狀态分兩個類型: 執行個體狀态和View狀态.

這裡有個最佳實踐: The Real Best Practices to Save/Restore Activity's and Fragment's state

不要把Fragment的執行個體狀态和View狀态混在一起處理.

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

Fragment狀态儲存入口:

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

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狀态恢複入口:

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

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

  1. 在Activity重新建立的時候, 恢複所有的Fragment狀态.
  2. 如果調用了FragmentManager的方法:

    saveFragmentInstanceState()

    , 傳回值得到的狀态可以用Fragment的

    setInitialSavedState()

    方法設定給新的Fragment執行個體, 作為初始狀态.
  3. 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的回調裡打了斷點, 發現調用棧如下:

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

FragmentManagerImpl

中,

moveToState()

方法的case Fragment.CREATED中:

調用了:

f.restoreViewState(f.mSavedFragmentState);

此時我沒有做任何儲存狀态的處理, 但是斷點中可以看出:

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

雖然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);
    }
}
           

本文完整例子相關實驗代碼可見:

HelloActivityAndFragment

中的State Restore Demo.

本文位址: Android Fragment使用(三) Activity, Fragment, WebView的狀态儲存和恢複

參考資料

Developer Android:

Android Fragment Reference

Android FragmentManager Reference

Posts:

Recreating an Activity

Activity的重新建立

從源碼角度剖析Fragment核心知識點

Fragment源碼閱讀筆記

The Real Best Practices to Save/Restore Activity's and Fragment's state

Android中儲存和恢複Fragment狀态的最好方法

Saving Android View state correctly

Tools:

icepick

作者: 聖騎士Wind

出處: 部落格園: 聖騎士Wind

Github: https://github.com/mengdd

微信公衆号: 聖騎士Wind

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