Fragment的状态保存和恢复

来源:互联网 发布:编程需要哪些基础知识 编辑:程序博客网 时间:2024/06/03 12:33

前言

我们知道,在activity中,当配置发生改变,比如屏幕方向发生变化时,activity会被销毁,然后重新创建。在activity中有两个方法用于保存和恢复状态,分别是:

    @Overrideprotected void onSaveInstanceState(Bundle outState) {    super.onSaveInstanceState(outState);}@Overrideprotected void onRestoreInstanceState(Bundle savedInstanceState) {    super.onRestoreInstanceState(savedInstanceState);}

当配置发生变化时,onSavaInstanceState会被调用,我们可以用它的参数outState来保存数据和状态;当activity重建的时候,会调用onRestoreInstanceState,然后利用它的参数savedInstanceState将activity恢复到销毁前的状态。另外在onCreate中也可以用于数据恢复,它的参数savedInstanceState和onRestoreInstanceState是一样的。
同样的google在fragment中也实现了类似的状态保存和恢复功能,以下我们分析下fragment的状态保存和恢复。

fragment的状态保存和恢复

实际上,fragment的状态保存和恢复机制和activity是完全一致的。说明解决方案之前,我们首先应该弄清楚下边的几个问题:

  1. 什么时候保存状态,什么时候恢复状态
  2. 保存和恢复什么状态(fragment的状态还是view的状态?)
  3. setRetainInstance(true)

什么时候保存状态,什么时候恢复状态

当系统认为你的fragment存在被销毁的可能时(不包括用户主动退出fragment导致其被销毁,比如按BACK键后fragment被主动销毁), onSaveInstanceState 就会被调用,给你一个机会来保存状态。以下几种情况可能导致fragment被异常销毁;

  1. 按HOME键返回桌面时
  2. 按菜单键回到系统后台,并选择了其他应用时
  3. 按电源键时
  4. 屏幕方向切换时

这四种情况中,前三种情况都是因为应用处于后台,根据Android系统的缓存机制,为了保持系统的流畅运行,处于后台的应用有很大的可能被清除,既然应用已经不在了,fragment自然也被销毁了;最后一种情况是由于屏幕方向切换导致配置改变,activity被销毁,fragment也随之被销毁了。
在这些情况下,我们就可以通过 onSaveInstanceState 方法将数据保存到它的参数bundle对象中了。以上触发onSaveInstanceState 的状况和activity完全一致。
有了保存,就应该有恢复。和activity不同的是,fragment没有onRestoreInstanceState方法,但是我们可以在onActivityCreated中恢复数据,它的参数中的bundle对象包含了在异常销毁前保存的数据。

保存和恢复什么状态(fragment的状态还是view的状态?)

在说明这个问题之前,我们应该做两个小实验

  1. 第一个实验:在fragment中的布局中加入一个EditText,设置宽高和id,不做其他设置,然后运行程序。在横屏的时候,在EditText输入随意的内容,然后横屏,你会发现EditText中的内容依然存在,难道fragment可以自动保存view的状态?
  2. 第二个实验:在fragment中的布局中加入一个EditText,只设置宽高,不做其他设置,然后运行程序。在横屏的时候,在EditText输入随意的内容,然后横屏,你会发现EditText中的内容不在了,难道fragment又不能保存view的状态了?
  3. 第三实验:在fragment的布局中加入一个EditText,一个TextView,一个Button,当点击button的时候,将Edittext的内容赋给TextView,代码如下:
    布局文件中三个view

    <EditTextandroid:id="@+id/editText"android:layout_width="wrap_content"android:layout_height="wrap_content"/><TextViewandroid:id="@+id/show"android:layout_width="wrap_content"android:layout_height="wrap_content"/>   <Buttonandroid:id="@+id/btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="按钮"/>
    @Nullable    @Override    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {        Log.d(TAG, ">>>onCreateView: ");        View view = inflater.inflate(R.layout.fragment_retain, null);        final EditText editText = (EditText) view.findViewById(R.id.editText);        final TextView textView = (TextView) view.findViewById(R.id.show);        Button btn = (Button) view.findViewById(R.id.btn);        btn.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                if (!editText.getText().toString().equals("")) {                    textView.setText(editText.getText());                }            }        });        return view;    }

横屏的时候输入内容,然后点击按钮,textview被赋值,然后旋转屏幕,textview的内容不见了。(实际情况是,第一次选择的时候内容还在,然后再次旋转回来以后内容就不在了,对于这个现象还没有找到原因)

通过以上的实验,我们发现即使我们没有在onSaveInstanceState 中显示的保存view的状态,但是有时候view的状态还是保存并恢复了,这是怎么回事那?

其实通过前两个实验我们可以看出view的状态是否能被自动保存和id是有关的,通过后两个实验我们发现除了和id有关应该还和其他的设置有关。

原来,在Android中当Activity的onSaveInstanceState调用的时候,Activity会自动收集View层级中每个View的状态。请注意只有在内部实现了View的保存/恢复状态方法的View才会被收集到。一旦onRestoreInstanceState方法被调用,Activity会把收集的数据发送回给View结构树中具有相同android:id配置的View。

这里要注意两点,
第一,view必须已经实现了保存/恢复状态方法

@Overridepublic Parcelable onSaveInstanceState() {Bundle bundle = new Bundle();// Save current View's state herereturn bundle;}@Overridepublic void onRestoreInstanceState(Parcelable state) {super.onRestoreInstanceState(state);// Restore View's state here}

我们平时常用的控件基本都实现了这两个方法,所以可以自动保存状态,如果我们要自定义View的话,也应该实现这两个方法用来保存状态。这里有个例外,textview需要声明android:freezeText=”true”才能保存和恢复状态。

第二,必须有Android:id属性
如果我们没有id属性的话,系统就没法找到我们的view,也就无法恢复状态。

知道了上边这一点之后,我们知道了,原来在Android的view设计中,view本身是具有状态保存和恢复功能的,所以我认为当我们在写代码的时候不应该打破这种设计模式,我们不应该在fragment的onSaveInstanceState中保存view的状态,而应该只用来保存fragment本身的状态和数据,也就是它的成员变量。这种思路是正确的。我们想一下,当我们将一个fragment加入到回退栈之后,然后在这个fragment中再打开另外一个fragment的时候,前一个fragment的视图会被销毁,但是实例还在,实例在,说明fragment的状态还在,视图销毁了,说明view的状态不在了;但是当我们从后面的fragment返回时会发现,view的状态又恢复了,我们并没有做任何额外的工作,这就说明了view是自动恢复状态的,不需要我们过多的干涉。

setRetainInstance

我们还可以通过在onCreate方法中设置setRetianInstance(true)的方法来达到保存数据的目的;当我们设置为true时,在屏幕方向改变时,fragment的实例不会被销毁,它只是被销毁了视图,并且从activity上解绑,然后重新创建的时候只会创建视图,因为实例还存在,所以不走onCreate方法。整个生命周期如下:

onDestroyView-->onCreateView-->onActivityCreated

需要特别注意的时,使用这种方法的时候,fragment不能加入到回退栈中,而且只适用于因为配置改变比如屏幕方向改变导致的fragment实例可能被销毁的状况,如果因为应用处于后台或者内存紧张等原因,fragment的实例还是可能被销毁的。具体原因可以浏览Stack Overflow

出于以上的原因,使用这种方式保存状态就有很大的局限性了。一般情况下,它可以用于保存activity的数据;比如一个activity中从网络上下载了很多数据,保留图片之类的,当屏幕方向改变时,调用onSaveInstance,保存数据到bundle中并不能有多大效果,因为bundle是一个适用于小数据的容器,如果过大可能有OOM的风险或其他问题。这个时候可以在onSaveInstance中开启一个没有视图的fragment,然后将数据保存到fragment中,然后当activity重建时,在onCreate中获得这个fragment的实例,取出数据,并且remove这个fragment,具体实现可以参考Android应用开发:Fragment与大型数据缓存

2 0