Activity的状态保存和恢复
Activity的状态什么时候需要保存和恢复
Activity的销毁一般分为两种情况:
当用户按返回按钮或你的Activity通过调用finish()销毁时,这属于正常销毁,此时是不需要恢复状态的,因为下次回来又是重新创建新的实例。
如果Activity当前被停止或长期未使用,或者前台Activity需要更多资源以致系统必须关闭后台进程恢复内存,系统也可能会销毁Activity,这属于非正常销毁,尽管Activity实例被销毁,但系统会保存其状态,这样,如果用户导航回该Activity,系统会使用保存了该Activity被销毁时的状态数据来创建Activity的新实例。
屏幕旋转、键盘可用性改变、 语言改变都可以归结为第二种情况;值得一提的是,如果需要模拟这种情况的Activity销毁,可以打开开发者选项,选择不保留活动(英文为Do not keep activities),即可模拟内存不足时的系统行为。
保存和恢复Activity状态
什么时候调用Activity的onSaveInstanceState()
屏幕旋转重建会调用onSaveInstanceState()
启动另一个activity: 当前activity在离开前会调用onSaveInstanceState()
按Home键的情形和启动另一个activity一样, 当前activity在离开前会onSaveInstanceState()
什么时候不调用Activity的onRestoreInstanceState()
屏幕旋转重建会调用onRestoreInstanceState()
启动另一个activity,返回时如果因为被系统杀死需要重建, 则会从onCreate()重新开始生命周期, 调用onRestoreInstanceState()
按Home键的情形和启动另一个activity一样,用户再次点击应用图标返回时, 如果重建发生, 则会调用onCreate()和onRestoreInstanceState()
什么时候都不调用
用户主动finish()掉的activity不会调用onSaveInstanceState(), 包括主动按back退出的情况.
新建的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 状态。
要测试应用能否在保持应用状态完好的情况下自行重启,您应该在应用中执行各种任务时调用配置变更(例如,更改屏幕方向)。 您的应用应该能够在不丢失用户数据或状态的情况下随时重启,以便处理如下事件:配置发生变化,或者用户收到来电并在应用进程被销毁很久之后返回到应用。
解决方案
你可能会遇到这种情况:重启应用并恢复大量数据不仅成本高昂,而且给用户留下糟糕的使用体验。 在这种情况下,您有两个其他选择:
在配置变更期间保留对象允许 Activity 在配置变更时重启,但是要将有状态对象传递给 Activity 的新实例。
自行处理配置变更阻止系统在某些配置变更期间重启 Activity,但要在配置确实发生变化时接收回调,这样,您就能够根据需要手动更新 Activity。
在配置变更期间保留对象
当 Android 系统因配置变更而关闭 Activity 时,不会销毁您已标记为要保留的 Activity 的片段。 您可以将此类片段添加到 Activity 以保留有状态的对象。
要在运行时配置变更期间将有状态的对象保留在片段中,请执行以下操作:
扩展 Fragment类并声明对有状态对象的引用。
将Fragment添加到 Activity。
重启 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绑定的对象,例如,Drawable、Adapter、View或其他任何与 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 处理配置变更,请在清单文件中编辑相应的 元素,以包含 android:configChanges 属性以及代表要处理的配置的值。android:configChanges 属性的文档中列出了该属性的可能值(最常用的值包括 "orientation"和 "keyboardHidden"
,分别用于避免因屏幕方向和可用键盘改变而导致重启)。
例如,以下清单文件代码声明的 Activity 可同时处理屏幕方向变更和键盘可用性变更:
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)。
@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,并且使用适合于新配置的资源(如提供资源中所述)。
参考资料