Android如何去处理运行时配置的改变?
一些设备的配置(比如屏幕方向,键盘的可用性及语言等)可以在运行时改变。当这样的改变发生时,Android会重新启动运行中的Activity(先调用onDestory()方法,然后调用onCreate()方法)。设计这种重新启动的行为是用来帮助你的应用适应新的配置,通过那些匹配新的设备配置的可选的资源自动重载你的应用。
要妥善处理这种重启行为,Activity 必须通过常规的Activity 生命周期方法恢复其之前的状态。在Activity被系统销毁之前(比如由于转动屏幕、切换语言等导致Activity重启)Android会调用onSaveInstanceState()方法,以此来保存关于应用状态的一些数据。然后可以在onCreate() 或者onRestoreInstanceState()方法恢复这些状态。
当然,onSaveInstanceState()方法并不是只有在Activity被系统销毁的时候才会被调用,只要存在潜在的可能性能被系统销毁,都会调用此方法。比如按home键,跳转到其他的Activity,锁屏等等,而且过段时间有可能因为内存的原因被系统销毁,在这种情况下,Android是有义务去保存当前Activity状态的,故onSaveInstanceState()方法也会被调用,尽管Activity在失去交互性的时候并没有立即被系统销毁。当然Activity由于转屏,切换语言而被系统销毁、重启的时候,onSaveInstanceState()肯定会被调用。但是如果Activity是由用户自己销毁的,比如按回退键,Android就没有义务去保存当前状态,onSaveInstanceState()自然也不会被调用。
两种选择
在处理Activity重启的时候,你可能会遇到这样的情况:重启应用和恢复大量数据可能会有高昂的成本开销,比如数据流量增加和用户体验下降等。在这样的情况下,你有两个其他的选择:
1、当配置改变时维持一个对象
当配置改变时允许Activity重启,但是携带一个有状态的对象给新的Activity实例。
2、当配置改变时自己去处理
当配置改变时阻止系统重启你的Activity,但是接收一个回调方法(如果配置确实改变的话),必要的话你可以手动地更新你的Activity。
当配置改变时维持一个对象
如果重启Activity要求你恢复大量的数据集,重新建立一个网络连接,或者执行一些密集型的操作,因为配置的改变而导致这样完整的重启会让用户觉得应用启动过慢,系统onSaveInstanceState()回调方法保存的Bundle对象并不能完全恢复activity的状态——它不是被设计成可以携带大型的对象(比如Bitmap),而且bundle里面的数据必须被序列化和反序列化,可能会消耗很多内存,从而导致配置的改变缓慢。在这样的情况下,当Activity由于配置改变而重启的时候,你可以通过维持一个Fragment碎片来减轻重新初始化Activity的负担。Fragment可以包含一个你想要维持的有状态的对象的引用。
当安卓系统因为配置改变而关闭Activity时,Activity 中被标记要维持的fragment并不会被销毁。你可以在activity里面添加这样的fragments去保存有状态的对象。
当运行配置改变时为了在fragment中维持有状态的对象,你可以这样做:
继承Framgment类和声明有状态对象的引用。
当fragment被创建的时候调用setRetainInstance(boolean)方法。查阅这个方法的API你可以发现,如果参数为true,即setRetainInstance(true),当activity销毁时,fragment的onDestory()方法不会被调用,onDetach()仍然会调用,也就是说fragment不会被销毁,只会和Activity解绑。当这个fragment再次和activity绑定的时候,onCreate()方法就不会再被调用了。系统会一直都维持着这个fragment对象。
将fragment添加到activity中。
当activity重启的时候使用FragmentManager检索这个fragment。
Fragment代码片段如下:
注意:虽然你可以保存任意的对象,但是绝对不能够保存和Activity绑定的对象,比如Drawable,Adapter,View或者任意的和Context有联系的对象。如果这样做,它会泄露原始Activity实例的所有视图和资源(资源泄露意味着应用一直抓着它们不放,也不能被回收,因此会损失很多的内存)。
然后使用FragmentManager将fragment添加到你的Activity中。当Activity因为运行配置改变而再次启动时你可以从fragment中获得这个对象。
Activity代码片段如下:
在上述代码片段中,Activity 的onCreate()方法添加一个fragment或者恢复一个fragment的引用。同时也在fragment中存储了一个有状态的对象。onDestroy()则更新了fragment实例中有状态的对象。
基本涵义
当配置改变时阻止系统重启你的Activity,但是接收一个回调方法(如果配置确实改变的话),必要的话你可以手动地更新你的Activity。
当配置改变时自己去处理
当一个特定的配置改变时,如果你的应用不需要更新资源而且你有一个性能的约束要求你避免Activity重启,那么你可以声明你的Activity自己去处理配置改变,阻止系统重启你的Activity。
应当要注意的是:自己去处理配置改变可能会让资源的选择变得更加困难,因为系统不会自动地应用可选的资源。当因为配置改变而必须避免重启时,这种技术应该被视为最后的手段,对于大多数应用程序不建议使用。
为了声明Activity去处理配置改变,只要在manifest清单文件里面的属性节点中添加android:configChanges= “” 这个属性,配置改变时就可以避免重启Activity.比如最常用的两个:
android:configChanges= “Orientation”表示屏幕方向改变时不会重启Activity
android:configChanges= “keyboardHidden ”表示输入法软键盘可用状态改变时不会重启Activity
若需申明多个配置值用“|”分开即可,如下:
只要配置上述的两个状态,当屏幕方向改变时或者键盘弹出或者消失都不会重启Activity。
更多详细的属性如下:
现在,当其中的一个配置改变时,MyActivity不会重启。MyActivity会收到一个回调onConfigurationChanged()。这个方法会传递一个Configuration对象,这个对象指定了新的设备配置。通过这个Configuration对象,你可以对界面上的资源做出恰当地更新。
当这个方法被调用的时候,Activity的Resources对象已经被新的配置更新,因此你可以很容易去重置UI上的元素而不必去重启Activity。
要注意一点,从3.2(API 13)开始,设备转屏的时候屏幕大小是会发生变化的。如果你的应用要在API13或者更高版本的sdk开发(指定minSdkVersion >= 13 和targetSdkVersion >=13)且在转屏的时候阻止Activity重启,你就必须这样声明:android:configChanges="orientation|screenSize".
如果你的应用targetSdkVersion在API12或者更低,也就是拿API12或者更低版本的sdk去编译,系统总是会自己处理配置改变(即使将这个应用运行到Android3.2或者更高版本也不会因为转屏而重启Activity)。因为在API13之前,转屏的时候screenSize不会发生改变。
举个例子:下面onConfigurationChanged()方法的实现会去检查当前设备的方向
Configuration代表了当前所有的配置,不仅仅是改变的那些配置,大多数时候,你不必介意配置是如何改变的,只要你提供了对应配置的可选资源(比如提供了横竖屏对应的布局/横竖屏对应的图片资源/不同的语言等等),只需要在onConfigurationChanged()回调方法简单地重新分配就可以了,因为当配置改变的时候,Resources对象已经更新了。
更多关于Configuration的信息请参考:
http://developer.android.com/intl/zh-cn/reference/android/content/res/Configuration.html
而这种方案也是有缺陷的, 因为其他配置的改变也可能导致Activity重启。比如语言的改变等等,而这些改变你未必全部都处理过。
最完美的当然是:不管Activity在什么情况下被销毁,重新启动的时候,还是原来的配方,还是熟悉的味道!
本文作者:项健(点融黑帮),一个热衷于android技术开发的程序猿,曾供职于索尼,主要负责系统app的维护和更新。现任职于点融网北京技术团队。