保存界面状态

在发生系统发起的 Activity 或应用销毁后,需要及时保存和恢复 Activity 的界面状态,这是用户体验的一个至关重要的部分。在这些情况下,用户希望界面状态保持不变,但是系统会销毁 Activity 及其中存储的任何状态。

要使系统行为符合用户预期,可以把 ViewModel 对象、onSaveInstanceState() 方法和/或本地存储空间结合起来使用,从而在发生此类应用和 Activity 实例转换后保持界面状态。在决定如何组合这些选项时,需要考虑界面数据的复杂程度、应用的用例以及检索速度与内存用量的权衡。

无论采用哪种方法,都应确保应用满足用户对其界面状态的预期,并提供流畅、简洁的界面(消除将数据载入界面过程中的延迟时间,尤其是在发生像旋转这样频繁的配置更改之后)。在大多数情况下,您应同时使用 ViewModel 和 onSaveInstanceState()。

本页讨论了用户对界面状态的预期,可用于保留状态的选项,以及每种选项的权衡因素和局限性。

用户预期和系统行为

根据用户执行的操作,他们会希望系统清除或保留 Activity 状态。在某些情况下,系统会自动执行用户预期的操作。但有时,系统会执行与用户预期相反的操作。

用户发起的界面状态解除

用户希望当他们启动 Activity 时,该 Activity 的暂时性界面状态会保持不变,直到用户完全关闭 Activity 为止。用户可以通过以下方式完全关闭 Activity:

  • 按返回按钮
  • 从“概览”(“最近使用的应用”)屏幕中滑动关闭 Activity
  • 从 Activity 向上导航
  • 从“设置”屏幕中终止应用
  • 完成某种“完成”Activity(由 Activity.finish() 提供支持)

在这些完全关闭的情况下,用户会认为他们已经永久离开 Activity,如果他们重新打开 Activity,会希望 Activity 以干净的状态启动。系统在这些关闭场景中的基础行为符合用户预期,即 Activity 实例将连同其中存储的任何状态以及与该 Activity 关联的任何已保存实例状态记录一起被销毁并从内存中移除。

关于完全关闭的此规则有一些例外情况,例如用户可能希望浏览器为他们打开的是他们在使用返回按钮退出浏览器之前查看的网页。

系统发起的界面状态解除

用户希望 Activity 的界面状态在发生配置更改(例如旋转或切换到多窗口模式)后保持不变。但是,默认情况下,系统会在发生此类配置更改时销毁 Activity,从而清除存储在 Activity 实例中的任何界面状态。要详细了解设备配置,请参阅配置参考页面。请注意,您可以替换针对配置更改的默认行为,但不建议这样做。如需了解详情,请参阅自行处理配置更改

如果用户暂时切换到其他应用,稍后再返回到您的应用,他们也会希望 Activity 的界面状态保持不变。例如,用户在您的搜索 Activity 中执行搜索,然后按主屏幕按钮或接听电话,当他们返回搜索 Activity 时,希望看到搜索关键字和结果仍在原处,并和之前完全一样。

在这种情况下,您的应用会被置于后台,系统会尽最大努力将您的应用进程留在内存中。但是,当用户转而去与其他应用进行互动时,系统可能会销毁该应用进程。在这种情况下,Activity 实例连同其中存储的任何状态都会一起被销毁。当用户重新启动应用时,Activity 会出乎意料地处于干净状态。要详细了解进程终止行为,请参阅进程和应用生命周期

用于保留界面状态的选项

当用户对界面状态的预期与默认系统行为不符时,您必须保存并恢复用户的界面状态,以确保系统发起的销毁对用户完全透明。

按照以下几个会影响用户体验的维度考量,用于保留界面状态的每个选项都有所差异:

ViewModel 已保存实例状态 持久性存储空间
存储位置 在内存中 已序列化到磁盘 在磁盘或网络上
在配置更改后继续存在
在系统发起的进程终止后继续存在
在用户完成 Activity 关闭/onFinish() 后继续存在
数据限制 支持复杂对象,但是空间受可用内存的限制 仅适用于基元类型和简单的小对象,例如字符串 仅受限于磁盘空间或从网络资源检索的成本/时间
读取/写入时间 快(仅限内存访问) 慢(需要序列化/反序列化和磁盘访问) 慢(需要磁盘访问或网络事务)

使用 ViewModel 处理配置更改

ViewModel 非常适合在用户正活跃地使用应用时存储和管理界面相关数据。它支持快速访问界面数据,并且有助于避免在发生旋转、窗口大小调整和其他常见的配置更改后从网络或磁盘中重新获取数据。要了解如何实现 ViewModel,请参阅 ViewModel 指南

ViewModel 将数据保留在内存中,这意味着成本要低于从磁盘或网络检索数据。ViewModel 与一个 Activity(或其他生命周期所有者)相关联,在配置更改期间保留在内存中,系统会自动将 ViewModel 与发生配置更改后产生的新 Activity 实例相关联。

当用户退出您的 Activity 或 Fragment 时,或者在您调用 finish() 的情况下,系统会自动销毁 ViewModel,这意味着状态会被清除,正如用户在这些场景中所预期的一样。

与已保存实例状态不同,ViewModel 在系统发起的进程终止过程中会被销毁。因此,您应将 ViewModel 对象与 onSaveInstanceState()(或其他一些磁盘持久性功能)结合使用,并将标识符存储在 savedInstanceState 中,以帮助视图模型在系统终止后重新加载数据。

如果您已有用于在发生配置更改后存储界面状态的内存中解决方案,则可能不需要使用 ViewModel。

使用 onSaveInstanceState() 作为后备方法来处理系统发起的进程终止

onSaveInstanceState() 回调会存储一些数据,如果系统销毁后又重新创建界面控制器(如 Activity 或 Fragment),则需要使用这些数据重新加载该控制器的状态。要了解如何实现已保存实例状态,请参阅 Activity 生命周期指南中的“保存和恢复 Activity 状态”。

已保存实例状态捆绑包在配置更改和进程终止后都会保留,但受限于存储容量和速度,因为 onSavedInstanceState() 会将数据序列化到磁盘。如果序列化的对象很复杂,序列化会占用大量的内存。因为此过程在配置更改期间发生在主线程上,所以如果耗时太长,序列化可能会导致丢帧和视觉卡顿。

请勿将 onSavedInstanceState() 用于存储大量的数据(如位图),也不要用于存储需要冗长的序列化或反序列化操作的复杂数据结构,而是只能用于存储基本类型和简单的小对象,例如字符串。因此,请使用 onSaveInstanceState() 存储最少量的数据(例如 ID),如果其他持久性机制失效,需要使用这些数据来重新创建必要的数据以将界面恢复到之前的状态。大多数应用都应实现 onSaveInstanceState() 来处理系统发起的进程终止。

根据应用的用例,您可能完全不需要使用 onSaveInstanceState()。例如,浏览器可能会将用户带回他们在退出浏览器之前正在查看的确切网页。如果 Activity 表现出这种行为,则您可以放弃使用 onSaveInstanceState(),改为在本地保留所有内容。

此外,如果您从 intent 打开 Activity,则当配置发生更改以及系统恢复该 Activity 时,会将 extra 捆绑包传送给该 Activity。在 Activity 启动时,如果一段界面状态数据(例如搜索查询)作为 intent extra 传入,则您可以使用 extra 捆绑包而不是 onSaveInstanceState() 捆绑包。要详细了解 intent extra,请参阅 Intent 过滤器和 Intent 过滤器

在上述任一情况下,您仍然可以使用 ViewModel 来避免因在配置更改期间从数据库重新加载数据而浪费周期时间。

如果要保留的是简单的轻量级界面数据,那么您可以单独使用 onSaveInstanceState() 来保留状态数据。

注意:您现在可以通过 ViewModel 的已保存状态模块(目前为 Alpha 版)在 ViewModel 对象中提供对已保存状态的访问途径。已保存状态可通过 SavedStateHandle 对象来访问。您可以在 Android 生命周期感知型组件 Codelab 中查看其使用方式。

针对复杂或大型数据使用本地持久性存储来处理进程终止

只要您的应用安装在用户的设备上,持续性本地存储(例如数据库或共享偏好设置)就会继续存在(除非用户清除应用的数据)。虽然此类本地存储空间会在系统启动的活动和应用进程终止后继续存在,但由于必须从本地存储空间读取到内存,因此检索成本高昂。这种持久性本地存储通常已经属于应用架构的一部分,用于存储您打开和关闭 Activity 时不想丢失的所有数据。

ViewModel 和已保存实例状态均不是长期存储解决方案,因此不能替代本地存储空间,例如数据库。您只应该使用这些机制来暂时存储瞬时界面状态,对于其他应用数据,应使用持久性存储空间。请参阅应用架构指南,详细了解如何充分利用本地存储空间长期保留您的应用模型数据(例如在重启设备后)。

管理界面状态:分而治之

您可以通过在各种类型的持久性机制之间划分工作,高效地保存和恢复界面状态。在大多数情况下,这些机制中的每一种都应存储 Activity 中使用的不同类型的数据,具体取决于数据复杂度、访问速度和生命周期的权衡:

  • 本地持久性存储:存储在您打开和关闭 Activity 时不希望丢失的所有数据。
    • 示例:歌曲对象的集合,其中可能包括音频文件和元数据。
  • ViewModel:在内存中存储显示关联界面控制器所需的所有数据。
    • 示例:最近搜索的歌曲对象和最近的搜索查询。
  • onSaveInstanceState():存储当系统停止后又重新创建界面控制器时轻松重新加载 Activity 状态所需的少量数据。这里指的是将复杂的对象保留在本地存储空间中,并将这些对象的唯一 ID 存储在 onSaveInstanceState() 中,而不是存储复杂的对象。
    • 示例:存储最近的搜索查询。

例如,假设有一个用于搜索歌曲库的 Activity。应按如下方式处理不同的事件:

当用户添加歌曲时,ViewModel 会立即委托在本地保留此数据。如果新添加的这首歌曲应显示在界面中,则您还应更新 ViewModel 对象中的数据以表明该歌曲已添加。切记在主线程以外执行所有数据库插入操作。

当用户搜索歌曲时,针对界面控制器从数据库加载的任何复杂歌曲数据都应立即存储在 ViewModel 对象中。您还应将搜索查询本身保存在 ViewModel 对象中。

当 Activity 进入后台时,系统会调用 onSaveInstanceState()。您应将搜索查询保存在 onSaveInstanceState() 捆绑包中。该少量数据很容易保存。这也是使 Activity 恢复到当前状态所需的所有信息。

恢复复杂的状态:重组碎片

当到了用户该返回 Activity 的时候,重新创建 Activity 存在两种可能情况:

  • 在系统停止 Activity 后重新创建该 Activity。该 Activity 将查询保存在 onSaveInstanceState() 捆绑包中,并且应将查询传递给 ViewModelViewModel 发现它没有缓存搜索结果,并使用指定的搜索查询委托加载搜索结果。
  • 在配置更改后创建 Activity。该 Activity 将查询保存在 onSaveInstanceState() 捆绑包中,而且 ViewModel 已缓存搜索结果。您将查询从 onSaveInstanceState() 捆绑包传递到 ViewModel,以此确定它已加载必要的数据,且无需要从数据库重新查询数据。

注意:最初创建 Activity 时,onSaveInstanceState() 捆绑包不包含任何数据,且 ViewModel 对象为空。创建 ViewModel 对象时,您将传递空白查询,以此告知 ViewModel 对象尚没有要加载的数据。因此,Activity 以空状态启动。

其他资源

要详细了解如何保存界面状态,请参阅以下资源。

博客