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={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恢复方法的调用时机

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状态保存入口:

fragment-state-save-entries.png

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-state-restore-entries.png

三个恢复的入口和三个保存的入口刚好对应.

  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-state-debug-onRestore.png

FragmentManagerImpl中, moveToState()方法的case Fragment.CREATED中:
调用了: f.restoreViewState(f.mSavedFragmentState);
此时我没有做任何保存状态的处理, 但是断点中可以看出:

fragment-states.png

虽然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.

参考资料

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

推荐阅读更多精彩内容