Android Activity 重建之状态保存与恢复
为什么要重建 Activity ?
Activity 负责用户界面,提供视图显示和接收用户输入。用户界面的显示和交互依赖于系统的配置,当系统的配置发生了变化,就需要修改用户界面。Android 默认通过重建 Activity,用同类型的新的 Activity 对象替换旧的 Activity 对象的方式来将系统配置的最新状态应用到用户界面上。
会引发系统重建 Activity 的系统配置有很多,比如屏幕方向、系统语言、字体等。当这些配置发生变化,系统就进入重建 Activity 的流程。
除了系统配置的变更,系统回收后台进程导致 Activity 销毁,当用户重新回到后台的应用的时候,也会触发重建流程。
要注意,这个销毁的过程是静默的、“未经用户允许的”,是系统自主行为。严格来说,Android 应该只在内存不足时回收后台进程,但是实际上生产装载 Android 系统的厂商有着自己的考量。比如长时间在后台运行的 App ,几小时甚至几天都没有再打开它,他们就会考虑让系统回收其进程;又或者用户希望系统的运行地省电,那么后台的程序则会更容易被回收。
相比之下,配置变更引起的重建是一个确定的过程,配置变更就立刻重建;而回收后台进程之后的重建则取决于用户是否重新回到后台应用,以及设备厂商想不想让系统重建。如果他们不想,就会修改 Android 的源代码,执行他们自己的策略。
状态是什么?
状态指的是 Acticity 内会可能会变化的细节,比如界面上显示的文本、图片,用户在输入框输入的文本、勾选的选择框,用户添加或删除的数据,也包括无需用户参与,程序自主发生的改变。
当 Activity 发生了变化,而之后系统又重建了 Activity,新的替换旧的,一切回到我们初始化 Activity 时的状态,所有的改变都会丢失。
我们想要将旧的 Activity 发生的变化应用到新的 Activity 上,就需要保存一个数据来承载发生的变化并在之后使用该数据的方法。Android 的工程师也想到了这些,所以提供了这样的功能。
如何保存与恢复?
onSaveInstanceState(Bundle)
和 onRetainNonConfigurationInstance()
两个方法就是用来保存 Activity 状态的,还有两个与前两者一一对应的 onRestoreInstanceState(Bundle)
与 getLastNonConfigurationInstance()
方法是获取状态的。
既然准了备两个组合,其功能想来是有差异的,虽然都有在重建 Activity 时保存与传递数据的功能,但是它们应对不同的场景。
一个是应对 Activity 进程被回收后在新的进程重建 Activity 的场景,另一个是应对系统状态变化后在同一进程重建 Activity 的场景。但是它们功能又有重合的地方,前者同时也具备后者的功能,但是我们应该让它们各司其职。
onSaveInstanceState()
和 onRestoreInstanceState()
是如何工作的?
在 Activity 的重建过程中,旧的 Activity 对象将被销毁之际,也即在调用其生命周期方法 onStop()
之后 与 onDestory()
之前,系统调用其 onSaveInstanceState()
方法,将 Bundle
类型的数据容器传递进去,该 Activity 将变化的状态存放到该容器,随后该 Activity 被销毁,该过程即“保存状态”;
只要 Activity 是“被动销毁”或“有被动销毁的可能”,系统就一定会保存状态,以便之后重建 Activity 时恢复状态,它们分别对应着“系统配置的变化”与“系统回收后台进程”对 Activity 的影响。回到启动器桌面、跳转到当前应用或其他应用的 Activity,都会使当前 Activity 进入后台,有被回收的可能,从而触发该方法的调用;而用户主动终止 Activity 进程导致 Activity 被销毁,明确不会重建 Activity,系统则不会调用此方法。
之后在合适的时机,系统重建 Activity,新的 Activity 对象被创建,系统
会调用其 onRestoreInstanceState()
方法,其调用时机是在 onStart()
与 onResume()
之间,将 Bundle
类型的数据容器传递进去,该 Activity 从中取出旧的 Activity 状态应用于自身,该过程即“恢复状态”。
onRestoreInstanceState()
只在 Activity 被重建时才会调用,和 onSaveInstanceState()
是成对地被调用。而当用户主动结束 Activity 进程,则不会重建 Activity,自然也无法调用。又或者 Activity 是正常新建的,此方法也不执行。
这一组方法是为跨进程边界的数据传递所准备的功能,这两者都接收一个 Bundle
类型的参数,它是一个存放数据的容器。因为进程间的内存空间是独立的,不能直接传递数据引用,故而所有存放到该容器的数据都必须是可被序列化的,以便在进程间传递数据。
另外,即使系统配置更改所触发的 Activity 重建过程所涉及的两个 Activities 对象在同一进程,系统也会调用这两个方法,但数据的传递方式不是引用传值,Bundle
还是被通过序列化的方式传递到主线程,再由主线程传递回来,再经历反序列化,转换为 Bundle
对象。
需要格外注意,使用 Bundle
传递数据有大小数据限制,上限为 1Mb,它是为简化进程间通信进行设计的,不是为了拷贝大量数据。这意味着我们需要分析 Activity 的状态,仅传递恢复 Activity 状态的必要数据,而不是包含所有状态的数据,非必要数据可以通过文件存储或数据库的方式传递。
比如说视图展示一个列表,有很多项,其数据由一个 List
对象维护,用户当前滚动到了列表视图的特定位置。
此时若应用进入后台,并被回收,我们不能将列表数据和定位信息都保存起来。系统回收 Activity 进程的目的往往是为了释放内存,我们却留存一份可能会占用比较大内存的数据,就违背了系统的意图,还很有可能超出传递数据的上限。
恢复视图的必要数据是列表的定位,我们仅保存并传递这个定位即可。至于列表的数据,保存到数据库或者序列化后保存到文件中。当调用新的 Activity 的 onRestoreInstanceState()
方法时,我们读取到了定位数据,然后在从文件中读取缓存的序列化数据,反序列化后渲染到列表视图,再根据定位信息恢复列表视图的位置。
尽量不要在 onCreate(Bundle)
生命周期方法中恢复数据
系统在重建 Activity 过程中,在调用 onCreate()
时,也会传递 Bundle
数据容器,此时 Bundle
有值,是先前“保存状态”时定义的;但是如果 Activity 不是重建的,而是正常新建的,那这个值就是 null
,如果不经验证就使用,程序会崩溃。
onRetainNonConfigurationInstance()
和 getLastNonConfigurationInstance()
如何工作?
它们是为同一进程中的 Activity 重建时传递数据所准备的功能,可以传递任何数据类型,是引用传值。
系统调用旧的 Activity 的 onRetainNonConfigurationInstance()
方法,该方法返回一个任意数据类型,系统保存它;新的 Activity 调用 getLastNonConfigurationInstance()
获取保存的数据,并做类型转换以取用数据。
onRetainNonConfigurationInstance()
的调用事件在 onSaveInstanceState()
之后,在 onDestory()
之前; getLastNonConfigurationInstance()
在 Activity 的任何生命周期方法中都能访问。
通过这种方式,我们可以传递承载 Activity 所有的状态的数据,并将该数据应用到重建的 Activity 中。
这一组方法永远是 Activity 在前台时被调用,与前面所提到的那组方法唯一重叠的地方就是在受到系统参数变化的影响时,系统会调用 onSaveInstanceState()
与 onRetainNonConfigurationInstance()
两个方法来“保存状态”,但是 另外的两个恢复状态的方法,只有 onRestoreInstanceState()
由系统调用,而 getLastNonConfigurationInstance()
的调用时机掌握在我们手里。
注意 Manifest 声明文件中声明 activity 的 android:configChanges 属性对 Activity 重建过程的影响
configChanges
对应着系统属性的变化,声明该属性意味着允许当前 Activity 响应指定的系统属性变化,而不是在这些属性变化时重建 Activity。该属性允许多个值,使用 |
分隔。
<activity
android:name=".MyActivity"
android:configChanges="orientation|screenSize"
android:label="@string/app_name">
上面的生命告知系统,该 Activity 希望响应屏幕方向和屏幕大小的变化,这会使系统在属性变化时调用 Activity 的 onConfigurationChanged()
方法,随后 Activity 调用关联视图的 onConfigurationChanged()
方法。
这些方法接收 Configuration
类型的对象,它承载着所有代表系统最新配置的数据,使用这些数据对系统属性变更作出响应。
怎么组合使用上面的两组方法?
使用 onSaveInstanceState()
和 onRestoreInstanceState()
恢复因系统回收 Activity 进程导致重建的 Activity 的状态。
使用 onRetainNonConfigurationInstance()
和 getLastNonConfigurationInstance()
恢复因系统状态变化后在同一进程重建的 Activity 的状态。
维护一个 isRestored
字段表示是否已恢复 Activity 状态。
在 onCreate()
方法中调用 getLastNonConfigurationInstance()
取得数据,如果取得了数据,说明 Activity 一定是在当前进程中重建的,然后为 Activity 恢复状态,设置为 isRestored
为 true;如果数据为空说明是在新进程中重建的,什么也不做。
在 onRestoreInstanceState()
中判断 isRestored
,若为 true,说明 Activity 是因为系统配置变化被重建的,已经使用了 getLastNonConfigurationInstance()
的数据恢复了状态;否则就是 Activity 进程被回收后在新进程重建的,需要在 onRestoreInstanceState()
方法中恢复。