Android中的狀態保存和恢復, 包括Activity和Fragment以及其中View的狀態處理. Activity的狀態除了其中的View和Fragment的狀態之外, 還需要用戶手動保存一些成員變數. Fragment的狀態有它自己的實例狀態和其中的View狀態, 因為其生命周期的靈活性和實... ...
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={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()
方法在如下情形下會調用:
- 屏幕旋轉重建: 先save再restore.
- 啟動另一個activity: 當前activity在離開前會save, 返回時如果因為被系統殺死需要重建, 則會從onCreate()重新開始生命周期, 調用onRestoreInstanceState(); 如果沒有重建, 則不會調用onCreate(), 也不會調用onRestoreInstanceState(), 生命周期從onRestart()開始, 接著onStart()和onResume().
- 按Home鍵的情形和啟動另一個activity一樣, 當前activity在離開前會save, 用戶再次點擊應用圖標返回時, 如果重建發生, 則會調用onCreate()和onRestoreInstanceState(); 如果activity不需要重建, 只是onRestart(), 則不會調用onRestoreInstanceState().
Activity恢復方法的調用時機
activity的onSaveInstanceState()
和onRestoreInstanceState()
方法在如下情形下不會調用:
- 用戶主動finish()掉的activity不會調用onSaveInstanceState(), 包括主動按back退出的情況.
- 新建的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狀態保存入口:
Fragment的狀態保存入口有三個:
- Activity的狀態保存, 在Activity的
onSaveInstanceState()
里, 調用了FragmentManger的saveAllState()
方法, 其中會對mActive中各個Fragment的實例狀態和View狀態分別進行保存. - FragmentManager還提供了public方法:
saveFragmentInstanceState()
, 可以對單個Fragment進行狀態保存, 這是提供給我們用的, 後面會有例子介紹這個. 其中調用的saveFragmentBasicState()
方法即為情況一中所用, 圖中已畫出標記. - FragmentManager的
moveToState()
方法中, 當狀態回退到ACTIVITY_CREATED
, 會調用saveFragmentViewState()
方法, 保存View的狀態.
moveToState()
方法中有很長的switch case, 中間不帶break, 基本是根據新狀態和當前狀態的比較, 分為正向創建和反向銷毀兩個方向, 一路沿著多個case走下去.
Fragment狀態恢復入口:
三個恢復的入口和三個保存的入口剛好對應.
- 在Activity重新創建的時候, 恢復所有的Fragment狀態.
- 如果調用了FragmentManager的方法:
saveFragmentInstanceState()
, 返回值得到的狀態可以用Fragment的setInitialSavedState()
方法設置給新的Fragment實例, 作為初始狀態. - 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的回調里打了斷點, 發現調用棧如下:
在FragmentManagerImpl
中, moveToState()
方法的case Fragment.CREATED中:
調用了: f.restoreViewState(f.mSavedFragmentState);
此時我沒有做任何保存狀態的處理, 但是斷點中可以看出:
雖然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狀態的最好方法
Handling Configuration Changes with Fragments
Saving Android View state correctly
Tools:
icepick