处理运行时状态的改变

英文原文地址 http://developer.android.com/guide/topics/resources/runtime-changes.html

一些设备配置会在运行时改变(比如屏幕方向、键盘可用性和语言)。当发生这些改变时,Android会重新启动正在运行的Activity(onDestroy()会被调用,之后是onCreate())。设计重新启动行为是为了通过自动重新加载你的应用从而适应新的配置,重新加载的应用会采用适应新配置的替代资源。

要正确地处理重新启动,非常重要的一点是你的activity在正常的Activity活动周期中存储下来它之前的数据,在活动周期中,Android在销毁你的activity之前会调用onSaveInstanceState()从而你可以保存有关应用状态的数据。你可以在onCreate()或者onRestoreInstanceState()中恢复数据。

为了测试你的应用是否状态完好地重新自启动,你应该在应用进行各种任务时改变配置状态(比如修改屏幕方向)。为了能够处理诸如配置改变或者用户接了一个电话,之后在你的应用进程被销毁之后回到应用之类的情况,你的应用需要能够在保证用户数据或状态无损的情况下随时重启。如果你想知道如何恢复activity的状态,请阅读Activity lifecycle.

但是你可能会遇到这样的情况:重新启动应用并恢复大量的数据开销非常大,而且会导致糟糕的用户体验。在这种情况下,你有另外两个选择:

  1. 在配置改变期间保留一个数据对象
    允许你的activity在配置改变时重启,但是要为你的activity保留一个有状态的对象(object)。
  2. 你自己来处理配置的改变
    在特定的配置改变时阻止系统重启应用,但是当配置改变时要接收回调函数,从而你可以按照需求手动地更新的你的activity。

在配置改变期间保留一个数据对象

如果重启应用需要你恢复大量的数据,重新建立网络连接或者进行其他密集型操作,那么由于配置改变而造成的一次完整的重启可能会是一次非常慢的用户体验。除此之外,你可能也无法使用系统通过onSaveInstanceState()回调函数为你保留的Bundle完整地恢复你的activity状态——它并非是被设计用来承载比较大的对象(比如bitmaps)的,而且其中的数据必须被序列化和反序列化,这些操作会占用非常多的内存并且使得配置切换缓慢。在这种情况下,你可以通过持有一个Fragment来减轻因为配置改变而重启activity时的负担。这个fragment将会保存你想要留存的有状态的对象的引用。

当安卓系统因为配置改变而关闭你的activity时,你标记为保留的fragment不会被销毁。你可以在你的activity中添加这样的fragment来保存有状态对象。

要想在运行配置改变期间在fragment中持有有状态对象:

  1. 继承Fragment类并声明对你的有状态对象的引用。
  2. 当fragment创建时调用setRetainInstance(boolean)。
  3. 把这个fragment添加到你的activity中。
  4. 当activity重启之后通过FragmentManager获取这个fragment。

举个例子,像下边这样定义你的 fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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把fragment添加到activity。当activity在配置改变期间重新启动后你可以从这个fragment获取数据对象。例如,像下边这样定义你的activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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()添加了一个fragment或者恢复了对它的引用。onCreate()也在fragment实例中存储了有状态对象。onDestroy()更新了持有的fragment中的有状态对象。

你自己来处理配置的改变

如果你的应用在特定的配置改变时不需要更新资源并且由于性能限制不允许重启activity,那么你可以声明你的activity自己来处理配置改变,这样可以阻止系统重启你的activity。

标注:亲自处理配置改变将会比使用可选资源困难得多,因为系统不会自动为你应用这些资源。当你一定要禁用由于配置改变而造成的重新启动时,这项技术可以被看做最后的手段,对于大多数应用并不推荐。

为了声明你的activity要处理一项配置更改,在你的manifest文件中编辑合适的标签来修改android:configChanges属性,填入合适的值以代表你要处理的配置。可选的值列在了android:configChanges属性的文档中(最常用的值是”orientation”来避免屏幕方向改变时activity的重启,以及”keyboardHidden”来避免键盘可用性发生改变时activity的重启)。你可以通过使用 | 把值隔开的方式声明多个值。

举个例子,下边的manifest代码声明了一个同时处理屏幕方向改变和键盘可用性改变的activity。

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

现在,当这些配置的其中一个发生改变时,MyActivity不会重启,而是接收到了一个对onConfigurationChanged()的调用。这个方法传递了一个Configuration对象,指定了新的配置。通过读取Configuration中的字段,你可以定义新的配置并且通过更新你的界面所用资源来做出合适的修改。当这个方法被调用时,你的activity的Resources对象会被更新并返回基于新配置的资源,从而你可以在系统不重启你的activity的情况下重置你的UI的元素。

注意:从Android3.2(API 版本13)开始,当设备在横屏和竖屏之间进行切换时”screen size”也会发生变化。因此在做API版本13或者更高(在属性minSdkVersion和targetSdkVersion中声明)的开发时,如果你想避免因为屏幕方向改变而引起的重启,你必须在”orientation”的基础上包含”screenSize”属性。也就是说,你必须声明android:configChanges=“orientation|screenSize”。但是如果你的应用是面向API版本12或者更低的,那么你的activity总能自己处理这种配置改变。(即使运行在Android版本3.2或者更高的设备上,配置改变也不会引起activity重启)

例如,下边的onConfigurationChanged()实现检查了当前设备的屏幕方向:

1
2
3
4
5
6
7
8
9
10
11
@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,你也可以设置其他对已经使用的配置来说合适的资源(就像Providing Resources说的那样)。

注意Configuration中的字段都是整型,并且他们都与Configuration类中的常量相匹配。你可以参考Configuration中具体的部分来获取每个字段使用的常量。

记住:当你声明你的activity以处理配置改变时,你有责任重新设置你为之提供备选的每个元素。如果你声明你的activity来处理屏幕方向变化并且有需要在横屏和竖屏之间切换的图片,你必须在onConfigurationChanged()中为每个元素重新分配新的资源。

如果你不需要基于这些配置修改做activity的更新,你可以不去实现onConfigurationChanged()。在这种情况下,配置更改前后使用的资源是相同的,你仅仅是避免了activity的重新启动。但是,你的应用应该总是能够关闭后再打开时原样保留之前的状态。因此,在正常的activity生命周期内,你不应该把这项技术当成保留状态的选择。不仅是因为你不能阻止一些其他的配置改变引起的应用重启,而且你还应该处理诸如用户离开应用,在用户再次开启应用之前应用就被销毁的情况。

想了解更多关于你在activity中可以处理的配置改变,请查阅android:configChanges文档和Configuration类。