Android的状态保存和恢复

Activity的状态保存和恢复

Activity的状态什么时候需要保存和恢复

Activity的销毁一般分为两种情况:

  1. 当用户按返回按钮或你的Activity通过调用finish()销毁时,这属于正常销毁,此时是不需要恢复状态的,因为下次回来又是重新创建新的实例。
  2. 如果Activity当前被停止或长期未使用,或者前台Activity需要更多资源以致系统必须关闭后台进程恢复内存,系统也可能会销毁Activity,这属于非正常销毁,尽管Activity实例被销毁,但系统会保存其状态,这样,如果用户导航回该Activity,系统会使用保存了该Activity被销毁时的状态数据来创建Activity的新实例。

屏幕旋转、键盘可用性改变、 语言改变都可以归结为第二种情况;值得一提的是,如果需要模拟这种情况的Activity销毁,可以打开开发者选项,选择不保留活动(英文为Do not keep activities),即可模拟内存不足时的系统行为。

保存和恢复Activity状态

什么时候调用Activity的onSaveInstanceState()

  1. 屏幕旋转重建会调用onSaveInstanceState()
  2. 启动另一个activity: 当前activity在离开前会调用onSaveInstanceState()
  3. 按Home键的情形和启动另一个activity一样, 当前activity在离开前会onSaveInstanceState()

什么时候不调用Activity的onRestoreInstanceState()

  1. 屏幕旋转重建会调用onRestoreInstanceState()
  2. 启动另一个activity,返回时如果因为被系统杀死需要重建, 则会从onCreate()重新开始生命周期, 调用onRestoreInstanceState()
  3. 按Home键的情形和启动另一个activity一样,用户再次点击应用图标返回时, 如果重建发生, 则会调用onCreate()和onRestoreInstanceState()

什么时候都不调用

  1. 用户主动finish()掉的activity不会调用onSaveInstanceState(), 包括主动按back退出的情况.
  2. 新建的activity, 从onCreate()开始, 不会调用onRestoreInstanceState().
basic-lifecycle-savestate.png

当你的Acivity开始被停止时,系统会调用onSaveInstanceState()方法,以便你的Activity可以使用键值对集合来保存状态信息。此方法默认实现了自动保存有关Activity的视图层次结构的状态信息,例如EditText中的文本信息或ListView的滚动位置。为了保存其他状态信息,你必须在onSaveInstanceState()方法中将键值对添加到Bundle对象。例如:

public class MainActivity extends Activity {
    static final String SOME_VALUE = "int_value";
    static final String SOME_OTHER_VALUE = "string_value";

    @Override
    protected void onSaveInstanceState(Bundle savedInstanceState) {
        // Save custom values into the bundle
        savedInstanceState.putInt(SOME_VALUE, someIntValue);
        savedInstanceState.putString(SOME_OTHER_VALUE, someStringValue);
        // Always call the superclass so it can save the view hierarchy state
        super.onSaveInstanceState(savedInstanceState);
    }
}

系统会在Activity被销毁前调用上述方法。然后系统会调用onRestoreInstanceState方法,我们可以从bundle中恢复状态:

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState);
    // Restore state members from saved instance
    someIntValue = savedInstanceState.getInt(SOME_VALUE);
    someStringValue = savedInstanceState.getString(SOME_OTHER_VALUE);
}

您可以选择实现系统在 onStart() 方法之后调的 onRestoreInstanceState(),而不是在 onCreate()期间恢复状态。系统只在存在要恢复的已保存状态时调用 onRestoreInstanceState(),因此你无需检查 Bundle是否为 null,实例状态也可以在ActivityonCreate()方法中恢复,但是onRestoreInstanceState()方法中是最方便的。这确保所有的初始化已经完成,并允许子类来决定是否使用默认实现。

注意:onSaveInstanceState和onRestoreInstanceState不能保证一起调用。当有可能销毁活动的可能性时,Android调用onSaveInstanceState()。 但是,有些情况下调用onSaveInstanceState,但不会销毁活动,因此不会调用onRestoreInstanceState。

保存和恢复Fragment状态

Fragments也有onSaveInstanceState()方法:

public class MySimpleFragment extends Fragment {
    private int someStateValue;
    private final String SOME_VALUE_KEY = "someValueToSave";
   
    // Fires when a configuration change occurs and fragment needs to save state
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putInt(SOME_VALUE_KEY, someStateValue);
        super.onSaveInstanceState(outState);
    }
}

然后,我们可以从onCreateView中保存的状态中取出数据:

public class MySimpleFragment extends Fragment {
   // ...

   // Inflate the view for the fragment based on layout XML
   @Override
   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.my_simple_fragment, container, false);
        if (savedInstanceState != null) {
            someStateValue = savedInstanceState.getInt(SOME_VALUE_KEY);
            // Do something with value if needed
        }
        return view;
   }
}

为了正确保存Fragments状态,我们需要确保在配置更改时避免不必要地重新创建Fragments。在配置更改后,在Activity中初始化的任何Fragments都需要通过tag进行查找:

public class ParentActivity extends AppCompatActivity {
    private MySimpleFragment fragmentSimple;
    private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (savedInstanceState != null) { // saved instance state, fragment may exist
           // look up the instance that already exists by tag
           fragmentSimple = (MySimpleFragment)  
              getSupportFragmentManager().findFragmentByTag(SIMPLE_FRAGMENT_TAG);
        } else if (fragmentSimple == null) { 
           // only create fragment if they haven't been instantiated already
           fragmentSimple = new MySimpleFragment();
        }
    }
}

注意:这就需要我们在使用事务将Fragment放入Activity中要包含一个用于查找的标记。

public class ParentActivity extends AppCompatActivity {
    private MySimpleFragment fragmentSimple;
    private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... fragment lookup or instantation from above...
        // Always add a tag to a fragment being inserted into container
        if (!fragmentSimple.isInLayout()) {
            getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.container, fragmentSimple, SIMPLE_FRAGMENT_TAG)
                .commit();
        }
    }
}
fragmentstatesaving.gif

245086265-57bc01e4c0a4a_articlex.gif

对于Fragment来说,有一些特殊情况不同于Activity,我觉得你需要知道这些情况。一旦Fragment从后退栈中返回,它的View 会被销毁,并重新创建

fragment-lifecycle.png

在这种情况下,Fragment并不会被销毁,只有Fragment中的View 会被销毁。 结果是,并不会发生任何实例状态的保存。但是上面展示的这些View在Fragment生命周期中被重新创建时,发生了什么?

在这种情况下,Fragment中的View 状态的保存/恢复会被内部调用。结果就是,每一个实现了内部View 状态保存/恢复的View ,将会被自动的保存并且恢复状态,例如带有android:freezeText="true"属性的EditText或者TextView。就之前完美显示的一样。

注意,在这种情况下,只有View 被销毁(并重建)了。Fragment仍然在这里,就像它内部的成员变量一样。所以你不需要对它们做任何事情。

View的状态保存和恢复

注意: 每一个你需要开启View状态保存和恢复的View设置android:id属性,不然它们的状态不能正确的恢复。

ListView

通常当你旋转屏幕时,应用程序将会丢失屏幕上列表的滚动位置和其他状态。要正确保留ListView的状态,可以在onPause()中存储实例状态并在onViewCreated中恢复状态,如下所示:

// YourActivity.java
private static final String LIST_STATE = "listState";
private Parcelable mListState = null;

// Write list state to bundle
@Override
protected void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    mListState = getListView().onSaveInstanceState();
    state.putParcelable(LIST_STATE, mListState);
}

// Restore list state from bundle
@Override
protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    mListState = state.getParcelable(LIST_STATE);
}


@Override
protected void onResume() {
    super.onResume();
    loadData(); // make sure data has been reloaded into adapter first
    // ONLY call this part once the data items have been loaded back into the adapter
    // for example, inside a success callback from the network
    if (mListState != null) {
        myListView.onRestoreInstanceState(mListState);
        mListState = null;
    }
}

注意:在调用onRestoreInstanceState之前,必须把数据源加载到适配器中,换句话说就是直到从网络或数据库加载数据之后,再让ListView调用onRestoreInstanceState。

RecyclerView

与ListView类似:

// YourActivity.java
public final static int LIST_STATE_KEY = "recycler_list_state";
Parcelable listState;

protected void onSaveInstanceState(Bundle state) {
     super.onSaveInstanceState(state);
     // Save list state
     listState = mLayoutManager.onSaveInstanceState();
     state.putParcelable(LIST_STATE_KEY, mListState);
}

protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    // Retrieve list state and list/item positions
    if(state != null)
        listState = state.getParcelable(LIST_STATE_KEY);
}

@Override
protected void onResume() {
    super.onResume();
    if (mListState != null) {
        mLayoutManager.onRestoreInstanceState(listState);
    }
}

处理运行时变更

有些设备配置可能会在运行时发生变化(例如屏幕方向、键盘可用性及语言)。 发生这种变化时,Android 会重启正在运行的 Activity(先后调用 onDestroy()onCreate())。重启行为旨在通过利用与新设备配置匹配的备用资源自动重新加载您的应用,来帮助它适应新配置。
要妥善处理重启行为,Activity 必须通过常规的Activity 生命周期恢复其以前的状态,在 Activity 生命周期中,Android 会在销毁 Activity 之前调用 onSaveInstanceState(),以便你保存有关应用状态的数据。 然后,你可以在 onCreate()onRestoreInstanceState()期间恢复 Activity 状态。
要测试应用能否在保持应用状态完好的情况下自行重启,您应该在应用中执行各种任务时调用配置变更(例如,更改屏幕方向)。 您的应用应该能够在不丢失用户数据或状态的情况下随时重启,以便处理如下事件:配置发生变化,或者用户收到来电并在应用进程被销毁很久之后返回到应用。

解决方案

你可能会遇到这种情况:重启应用并恢复大量数据不仅成本高昂,而且给用户留下糟糕的使用体验。 在这种情况下,您有两个其他选择:

  1. 在配置变更期间保留对象允许 Activity 在配置变更时重启,但是要将有状态对象传递给 Activity 的新实例。

  2. 自行处理配置变更阻止系统在某些配置变更期间重启 Activity,但要在配置确实发生变化时接收回调,这样,您就能够根据需要手动更新 Activity。

在配置变更期间保留对象

当 Android 系统因配置变更而关闭 Activity 时,不会销毁您已标记为要保留的 Activity 的片段。 您可以将此类片段添加到 Activity 以保留有状态的对象。
要在运行时配置变更期间将有状态的对象保留在片段中,请执行以下操作:

  1. 扩展 Fragment类并声明对有状态对象的引用。
  2. 在创建Fragment后调用 setRetainInstance(boolean)
  3. 将Fragment添加到 Activity。
  4. 重启 Activity 后,使用 FragmentManager检索片段。

例如,按如下方式定义片段:

public class RetainedFragment extends Fragment {

    // data object we want to retain
    private MyDataObject data;

    // this method is only called once for this fragment
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // retain this fragment
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}

注意:尽管你可以存储任何对象,但是切勿传递与 Activity绑定的对象,例如,DrawableAdapterView或其他任何与 Context关联的对象。否则,它将泄漏原始 Activity 实例的所有视图和资源。 (泄漏资源意味着应用将继续持有这些资源,但是无法对其进行垃圾回收,因此可能会丢失大量内存。)

然后,使用 FragmentManager将片段添加到 Activity。在运行时配置变更期间再次启动 Activity 时,您可以获得片段中的数据对象。 例如,按如下方式定义 Activity:

public class MyActivity extends Activity {

    private RetainedFragment dataFragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // find the retained fragment on activity restarts
        FragmentManager fm = getFragmentManager();
        dataFragment = (DataFragment) fm.findFragmentByTag(“data”);

        // create the fragment and data the first time
        if (dataFragment == null) {
            // add the fragment
            dataFragment = new DataFragment();
            fm.beginTransaction().add(dataFragment, “data”).commit();
            // load the data from the web
            dataFragment.setData(loadMyData());
        }

        // the data is available in dataFragment.getData()
        ...
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // store the data in the fragment
        dataFragment.setData(collectMyLoadedData());
    }
}

在此示例中,onCreate()
添加了一个片段或恢复了对它的引用。此外,onCreate()还将有状态的对象存储在片段实例内部。onDestroy()对所保留的片段实例内的有状态对象进行更新。

自行处理配置变更

如果应用在特定配置变更期间无需更新资源,并且因性能限制您需要尽量避免重启,则可声明 Activity 将自行处理配置变更,这样可以阻止系统重启 Activity。

:自行处理配置变更可能导致备用资源的使用更为困难,因为系统不会为您自动应用这些资源。 只能在您必须避免 Activity 因配置变更而重启这一万般无奈的情况下,才考虑采用自行处理配置变更这种方法,而且对于大多数应用并不建议使用此方法。

要声明由 Activity 处理配置变更,请在清单文件中编辑相应的 <activity>
元素,以包含 android:configChanges
属性以及代表要处理的配置的值。android:configChanges
属性的文档中列出了该属性的可能值(最常用的值包括 "orientation"和 "keyboardHidden"
,分别用于避免因屏幕方向和可用键盘改变而导致重启)。

例如,以下清单文件代码声明的 Activity 可同时处理屏幕方向变更和键盘可用性变更:

<activity android:name=".MyActivity"
          android:configChanges="orientation|keyboardHidden"
          android:label="@string/app_name">

现在,当其中一个配置发生变化时,MyActivity不会重启。相反,MyActivity会收到对 onConfigurationChanged()的调用。向此方法传递Configuration对象指定新设备配置。您可以通过读取 Configuration中的字段,确定新配置,然后通过更新界面中使用的资源进行适当的更改。调用此方法时,Activity 的 Resources对象会相应地进行更新,以根据新配置返回资源,这样,您就能够在系统不重启 Activity 的情况下轻松重置 UI 的元素。

注意:从 Android 3.2(API 级别 13)开始,当设备在纵向和横向之间切换时,“屏幕尺寸”也会发生变化。因此,在开发针对 API 级别 13 或更高版本(正如 minSdkVersion
targetSdkVersion
属性中所声明)的应用时,若要避免由于设备方向改变而导致运行时重启,则除了 "orientation"
值以外,您还必须添加 "screenSize"
值。 也就是说,您必须声明 android:configChanges="orientation|screenSize"
。但是,如果您的应用面向 API 级别 12 或更低版本,则 Activity 始终会自行处理此配置变更(即便是在 Android 3.2 或更高版本的设备上运行,此配置变更也不会重启 Activity)。

例如,以下 onConfigurationChanged() 实现检查当前设备方向:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks the orientation of the screen
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
        Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
    }
}

Configuration 对象代表所有当前配置,而不仅仅是已经变更的配置。大多数时候,您并不在意配置具体发生了哪些变更,而且您可以轻松地重新分配所有资源,为您正在处理的配置提供备用资源。 例如,由于 Resources 对象现已更新,因此您可以通过 setImageResource() 重置任何 ImageView,并且使用适合于新配置的资源(如提供资源中所述)。

参考资料

http://www.cnblogs.com/mengdd/p/4528417.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容