8.1.3 应用资源的动态匹配

通过上一小节的介绍可以了解到,利用Android应用资源的可适配性,可以实现应用的“一次打包,到处运行”,在运行时动态地适应设备的软硬件配置,降低了应用开发和部署的成本,提升了应用的适应能力。

但设备配置的改变可能会发生在程序运行的各个阶段,比如,在应用运行中,当用户转动手机时,设备的屏幕朝向配置可能会从垂直向变为水平向。在界面组件的默认设置中,任何设备配置项发生变化前,Android都会强制回收当前与用户交互的界面组件对象,并在新的设备配置项下重新构造和恢复该组件对象。

这种“简单粗暴”的适配策略,可以使得界面组件能够自动适应所有设备配置项的变更,极大地减轻了开发者的负担,提升了应用的兼容性。但该策略并不是一个“最优”策略,当一些设备属性频繁发生变化时(比如屏幕朝向可能会随着用户晃动设备而不断发生变化),会导致系统反复地销毁和重构界面组件对象,不仅降低了应用的性能,还会极大地影响应用与用户的交互。

为了使界面组件能够更高效更合理地适应各种配置,Android为开发人员提供了两种适配设备配置变更的手段。

❑快速缓存状态

第5章曾经介绍过,当系统资源消耗达到阈值时,界面组件对象及其所在的进程可能会被强制销毁。此时,可以通过Activity.onSaveInstanceState函数缓存界面组件状态,等到组件被重构时再通过Activity.onRestoreInstanceState函数进行恢复。为了保证界面状态数据的安全,Activity.onSaveInstanceState函数会将界面状态信息序列化到一个android.os.Bundle对象中,保存到系统的核心进程内,谨防由于应用进程被销毁而导致数据丢失。

同样的方式也可以用来保存设备配置变化之前界面组件的状态,但由于需要不断地序列化和反序列化,其实现效率较低。考虑到当设备配置发生变化时,界面组件对象所在的应用进程并不会被销毁,因此可以不必将数据存储到系统核心进程中去,从而跳过序列化的过程。

在Android中,设备配置项发生变化前,Activity.getLastNonConfigurationInstance函数会被调用。该函数的执行发生在Activity.onStop函数调用之后,Activity.onDestroy函数被执行前。开发者可以利用该函数来保存需要维持的界面组件状态,比如:


@Override

public Object onRetainNonConfigurationInstance(){

//读取并保存状态

final MyDataObject data=getStateData();

return data;

}


而设备配置变化完成后,界面组件对象会被重新构造,此时,可以通过派生Activity.getLastNonConfigurationInstance函数来读取变化前保存的状态对象,将界面状态组件恢复到被销毁之前。示例如下:


@Override

public void onCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);

//读取并恢复状态

final Object data=getLastNonConfigurationInstance();

if(data!=null){

setStateData(data);

}

}


❑避免组件的销毁和重构

通过快速缓存状态可以减少序列化带来的开销,帮助界面组件“更快”地恢复到被销毁之前的状态。但它本质上是一种“防御性”手段,不能完全避免界面组件对象被不断销毁和重构带来的负担。

在应用开发中,有很多资源项在不同的设备环境中都保持不变。比如,描述界面样式的layout文件在屏幕朝向不同时都会使用同一份资源文件。这就意味着,当某些设备配置发生变化时,有的界面组件对应的资源信息并不会发生改变,不需要进行重新构造。

在应用配置文件中,界面组件都可以通过组件配置项中的android:configChanges属性声明不关注的设备配置项,当这些配置项发生变化时不需要销毁和重构该组件对象。比如,如果一个组件不会随着屏幕朝向或键盘设置的变化而进行资源替换,就可以进行如下声明,以避免被系统重构:


<activity android:name="NonChangesActivity"

android:configChanges="orientation|keyboardHidden">


其中,android:configChanges的值是一个整数的标志位,可以通过或运算进行组合,每个标志位对应一个或多个配置项[1]

一旦在android:configChanges中进行了声明,对应的配置项发生变化时,Android就不再会强制回收该组件,而是调用该组件的Activity.onConfigurationChanged函数,通知配置变更事件,开发者可以通过重载该函数来自定义变更逻辑:


@Override

public void onConfigurationChanged(Configuration newConfig){

super.onConfigurationChanged(newConfig);

//处理屏幕朝向变化事件

if(newConfig.orientation==

Configuration.ORIENTATION_LANDSCAPE){

}else if(newConfig.orientation==

Configuration.ORIENTATION_PORTRAIT){

}

//处理键盘可用性变化事件

if(newConfig.keyboardHidden==

Configuration.KEYBOARDHIDDEN_NO){

}else if(newConfig.keyboardHidden==

Configuration.KEYBOARDHIDDEN_YES){

}

}


小贴士 从理论上来说,只要一个配置项没有对应的可替换资源,就应该将其对应的标志位计入界面组件的android:configChanges属性中。但从实践的角度来看,大部分配置项是很少在运行时发生变化的,比如时区、屏幕属性等。经常发生变化的配置项只有屏幕朝向Configration.orientation和键盘可用性Configration.keyboardHidden。

因此,开发者在配置界面组件时,需要额外关注这两个配置项,比如,如果界面组件没有为不同的屏幕朝向提供可替换的资源文件,那么一定要为android:configChanges添加orientation值,以提升性能。

[1]android:configChanges的值定义在android.content.res.Configuration类中,它和各个配置项之间的关系可参见:http://androidappdocs.appspot.com/reference/android/content/res/Configuration.html。