Android程序在运行期间设备的配置是可能发生改变的(例如屏幕的方向,键盘可用性,和语言等)。当这些配置发生变化时,Android会重启正在运行的Activity(先调用onDestory(),紧接着调用onCreate())。这个设计是为了让你的程序在配置发生变化时,使用不同的资源自动去适配新的配置机器。
正确的处理重启,一件很重要的事就是通过Activity正常的生命周期去恢复之前状态,你可以在Android销毁你的Activity前调用onSaveInstanceState()来保存程序的状态。然后你就可以在onCreate()或onRestoreInstanceState()调用期间恢复程序之前的状态了。
如果你想测试你程序重启之后的状态完整性,你可以在程序开着不同线程处理事务时主动改变配置(例如改变屏幕的方向)。你的程序应该在任何时候重启都不会丢失用户的状态或数据,例如配置改变或用户突然收到来电后程序的进程被销毁。你可以阅读关于Activity的生命周期学习怎样保存你的Activity状态。
不过,你可能遭遇过程序重启时需要恢复大量的数据导致而糟糕的用户体验。发生这种情况,你还有下面这两种方法:
1、配置改变时保留一个对象
允许你的Activity在配置改变时可以重启,然后在新建一个Activity时携带一个之前所有状态数据的类
2、你自己处理配置改变
配置改变时你不必重启你的Activity,而是收到一个回调方法,这样你就可以更新Activity需要更新的资源
配置改变时保存一个对象
如果重启你的activity需要恢复大量的数据、重建网络连接、或展示其他紧急的事务,那么重启将会非常慢。同时,你不可能在系统回调onSaveInstanceState()时保存整个activity的状态数据进Bundle里,它也不是设计来携带需要序列化和反序列化的大数据量对象(如bitmap对象)的,那需要开销大量的内存并使配置变化变慢。在这种情况下,你可以在保存一个Fragment来缓解因为配置改变导致activity重启的初始化负担。这个fragment可以持有你想保存的引用到一个状态对象里。
当Android系统因为配置改变销毁你的activity时,activity里那个你想要来保存数据的fragment并不会销毁。你可以增加这样的fragment在activity里来保护状态对象。
在配置改变期间保存一个状态对象的步骤:
1、继承Fragment类并声明你的状态引用
2、当fragment创建时调用setRetainInstance(boolean)方法
3、将上面的fragment放到你的activity里
4、当activity重启时用FragmentManager去保存这个fragment
例如下面这个例子:
public class RetainedFragment extends Fragment {
//我们想要的保存的数据对象
private MyDataObject data;
// 这个方法在fragment里只被调用一次
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 保存这个fragment
setRetainInstance(true);
}
public void setData(MyDataObject data) {
this.data = data;
}
public MyDataObject getData() {
return data;
}
}
慎重:当你保存任何对象时,最好不要保存那些跟Activity绑定的对象,例如Drawable, Adapter, View或者任何一个需要和Context绑定才能使用的对象。如果你保存了,意味着你会泄漏原来activity实体里的所有views和资源。(泄漏资源的意思是,你的程序持有这些对象将使它们没办法被垃圾回收,会造成内存泄漏。)
接着用FragmentManager把上面的fragment放在activity里。你可以在activity重新启动期间从fragment得到数据对象。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”);
// 第一次创建fragment和数据
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();
// 将数据保存在这个fragment里
dataFragment.setData(collectMyLoadedData());
}
}
在上面例子,onCreate()里创建一个fragment对象并在里面保存数据对象。然后在onDestory()方法内更新fragment里的数据对象。
你自己处理配置改变
如果你的程序在一些配置改变期间不需要更新资源,并且你有一些特殊的限制使你避免重启activity,那么就可以声明你自己来处理配置变化,系统也就不会重启你的activity。
注意:自己处理配置变化在使用其他的资源时会变得更困难,因为系统不会自动为你处理。这种技术你应该在万不得已时才用,大部分程序是不推荐使用的。
你可以在mainifest文件相应的标签里,声明android:configChanges属性来配置你想要处理的事件。adnroid:configChanges文档里列出了所有能够配置的属性(大多数人会用到”orientation”来避免因屏幕方向变化导致activity重启,和用”keyboardHidden”来防止键盘的可用性变化导致重启)。你可以用’|’字符隔开不同属性来声明多个配置。
例如,下面的配置是会使MyActivity检测到屏幕方向变化和键盘可用性变化:
<activity android:name=".MyActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">
现在,当上面那两个配置发生改变,MyActivity将不会重启。不过,MyActivity的onConfigurationChanged()方法会被调用。这个方法会通过一个Configuration对象来指定新设备的参数。通过读取Configuration的属性值,你可以确定新的配置并且用你的接口更新到合适的资源里。在这个方法调用时,你activity的资源对象将以新的配置为基础做更新,这样你就可以很容易的重置你的用户视图而不必重启activity。
注意:从Andorid 3.2(API level 13)开始,当设备的屏幕在垂直和水平方向切换时,屏幕尺寸(screen size)也会随之改变。因此从API level 13起(在mainifest里有声明minSdkVersion和targetSdkVersion属性),如果你想要避免activity在运行时重启,就需要在configChanges属性中,除了声明”orientation”外同时加上”screenSize”,如android:configChanges=”orientation|screenSize”。不过,如果你的程序的目标API在12或以下,你的activity也可以用这两个属性来避免重启(这个两个属性可以避免所有Android版本的因屏幕方向变化导致的重启)。
例如,下面的onConfigurationChanged()方法可以用来检测当前设备屏幕变化:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 检查屏幕的方向
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对象持有所有当前设备的配置,不仅仅是那些变化的。通常,你不用关心配置是怎样改变的,你只需从configuration里获取配置并重新分配你的所有资源文件就得了。比如,因为资源对象现在已经更新,你就可以用setImageResource()方法来重置你所有ImageView控件(资源文件可参考Providing Resources)。
需要注意的是,Configuration对象里的参数都是整形,它们和Cofiguration类里的常量相匹配。你可以从Configuration文档中找到可用的配置属性。
请记住:当你自己声明处理配置变化,你就有责任去更新那些有替代品的元素。如果你声明了你的activity去处理屏幕方向变化事件,且你有一些图片是需要在横竖屏切换时改变,那么你就必须在onConfiguration()方法调用期间重置那些控件资源。
如果你不需要再配置变化时更新你的程序,你也可以不用实现onConfigurationChanged()方法。在这种情况下,程序在配置变化后还会继续引用之前的资源,只是不重启你的activity。不过,你的程序最好可以被销毁和带着之前的状态重启,你不应该用这种技术在正常的生命周期里保持状态溜走。不止是因为你无法避免那些你没处理的变化会导致重启你的程序,且应该处理用户离开你程序的事件和在用户返回之前销毁它。
如果你想知道activity里有多少配置变化可以处理,看看android:configChanges和Configuration类。