[TOC]
保存UI状态
在系统启动的activity或应用程序销毁过程中及时保留和恢复activity的UI状态是用户体验的关键部分。在这些情况下,用户期望UI状态保持不变,但系统会破坏activity以及存储在其中的任何状态。
为了弥补用户期望和系统行为之间的差距,可以使用ViewModel对象、onSaveInstanceState()
方法和/或本地存储的组合来跨应用程序和activity实例转换持久化UI状态。决定如何组合这些选项取决于UI数据的复杂性、应用程序的用例以及检索速度和内存使用情况。
无论采用哪种方法,都应该确保应用程序满足用户对UI状态的期望,并提供流畅、快速的UI(避免在将数据加载到UI时的延迟,尤其是在频繁发生配置更改(如旋转)之后)。在大多数情况下,应该同时使用ViewModel和onSaveInstanceState()。
此页面讨论了用户对UI状态的期望,可用于保存状态的选项,每个的权衡和限制。
用户期望和系统行为
根据用户所采取的操作,他们要么希望清除activity状态,要么希望保留状态。在某些情况下,系统会自动执行用户所期望的操作。在其他情况下,系统执行与用户期望相反的操作。
用户启动的UI终止状态
用户期望在他们开始activity时,该activity的UI瞬态状态将保持不变,直到用户完全终止activity为止。用户可以通过以下方式完全终止activity:
- 按下back按钮
- 从Overview (Recents)屏幕上删除activity
- 从activity向上导航
- 从“设置”屏幕中杀死该应用
- 完全“完成”activity(由activity .finish()支持)
在这些完全终止案例中,用户的假设是他们已经永久地从activity中导航出去,如果他们重新打开该activity,他们希望该activity从一个干净的状态开始。这些终止场景的底层系统行为符合用户的期望——activity实例将被销毁并从内存中删除,同时删除存储在其中的任何状态以及与activity关联的任何保存的实例状态记录。
这条关于完全删除的规则也有一些例外——例如,用户可能希望浏览器在使用后退按钮退出浏览器之前将他们带到他们正在查看的确切页面。
系统启动的UI终止状态
用户希望activity的UI状态在整个配置更改期间保持不变,比如旋转或切换到多窗口模式。但是,默认情况下,当发生这样的配置更改时,系统会销毁activity,删除存储在activity实例中的任何UI状态。有关设备配置的更多信息,请参见Configuration reference page。注意,可以(虽然不建议)覆盖配置更改的默认行为。有关详细信息,请参见Handling the Configuration Change Yourself。
如果用户暂时切换到其他应用程序然后稍后返回您的应用程序,则用户还希望您的activity的UI状态保持不变。例如,用户在您的搜索activity中执行搜索,然后按下主页按钮或接听电话 - 当他们返回搜索activity时,他们希望找到搜索关键字并且结果仍然存在,就像之前一样。
在这种情况下,您的应用程序将放置在后台,系统会尽最大努力将您的应用程序进程保留在内存中。但是,当用户离开与其他应用程序交互时,系统可能会破坏应用程序进程。在这种情况下,activity实例以及存储在其中的任何状态都将被销毁。当用户重新启动应用程序时,activity意外处于干净状态。要了解有关进程死亡的更多信息,请参阅 Processes and Application Lifecycle。
用于保留UI状态的选项
当用户对UI状态的期望与默认系统行为不匹配时,您必须保存并恢复用户的UI状态,以确保系统启动的销毁对用户是透明的。
保存UI状态的每个选项都在以下影响用户体验的维度上有所不同:
ViewModel | Saved instance state | Persistent storage | |
---|---|---|---|
Storage location | in memory | serialized to disk | on disk or network |
Survives configuration change | Yes | Yes | Yes |
Survives system-initiated process death | No | Yes | Yes |
Survives user complete activity dismissal/onFinish() | No | No | Yes |
Data limitations | complex objects are fine, but space is limited by available memory | only for primitive types and simple, small objects such as String | only limited by disk space or cost / time of retrieval from the network resource |
Read/write time | quick (memory access only) | slow (requires serialization/deserialization and disk access) | slow (requires disk access or network transaction) |
使用ViewModel处理配置更改
当用户频繁使用应用程序时,ViewModel是存储和管理ui相关数据的理想工具。它允许快速访问UI数据,并帮助您避免在旋转,窗口大小调整和其他常见配置更改中从网络或磁盘中重新获取数据。要了解如何实现ViewModel,请参阅ViewModel指南。
ViewModel将数据保留在内存中,这意味着检索比ViewModel中的数据比磁盘或网络更合算。ViewModel与activity(或其他一些生命周期所有者)相关联 - 它在配置更改期间保留在内存中,系统自动将ViewModel与配置更改产生的新activity实例相关联。
当用户退出activity或fragment或调用finish()时,系统会自动销毁viewmodel,这意味着在这些场景中,状态将如用户所期望的那样被清除。
与保存的实例状态不同,ViewModel在系统启动的进程死亡期间被销毁。这就是为什么你应该将ViewModel对象与onSaveInstanceState()(或其他一些磁盘持久性)结合使用,在savedInstanceState中存储标识符以帮助views models在系统死亡后重新加载数据。
如果您已经在内存中准备好了用于跨配置更改存储UI状态的解决方案,那么您可能不需要使用ViewModel。
使用onSaveInstanceState()作为备份来处理系统启动的进程死亡
onSaveInstanceState()回调函数存储了重新加载UI控制器状态(如activity或fragment)所需的数据,如果系统销毁并稍后重新创建该控制器的话。要了解如何实现已保存的实例状态,请参阅Activity Lifecycle guide中的保存和恢复activity状态。
保存实例状态在配置更改和进程终止时,持续存在,但是由于onSavedInstanceState()将数据序列化到磁盘,所以受到存储量和速度的限制。如果序列化的对象比较复杂,那么序列化会消耗大量内存。由于这个过程在配置更改期间发生在主线程上,如果序列化耗时太长,可能会导致帧丢失和视觉卡顿。
不要使用onSavedInstanceState()存储大量数据,如位图,也不要使用需要长时间序列化或反序列化的复杂数据结构。相反,只存储基本类型和简单的小对象,比如String。因此,使用onSaveInstanceState()存储少量必要的数据,例如ID,以便在其他持久性机制失败时重新创建必要的数据,将UI恢复到以前的状态。大多数应用程序应该实现onSaveInstanceState()来处理系统启动的进程死亡。
根据应用程序的使用情况,您可能根本不需要使用onSaveInstanceState()。例如,浏览器可能会将用户带回他们退出浏览器之前正在查看的准确页面。如果您的activity以这种方式运行,您可以放弃使用onSaveInstanceState(),而是在本地保存所有内容。
此外,当您从intent打开一个activity时,the bundle of extras在配置更改和系统恢复该activity时都被交付给该activity。如果在启动activity时将UI状态数据(例如搜索查询)作为intent extra传递进来,那么可以使用extras bundle而不是onSaveInstanceState() bundle。要了解有关intent extras的更多信息,请参阅Intent和Intent Filters。
使用本地持久(存储)处理复杂或大型数据来应对进程死亡
只要您的应用程序安装在用户的设备上,持久性本地存储(例如数据库或shared preferences)就会存在(除非用户清除应用程序的数据)。虽然这样的本地存储在系统启动的activity和应用程序进程死亡后仍然存在,但检索起来可能很昂贵,因为它必须从本地存储器读取到内存中。通常,此持久性本地存储可能已经是应用程序体系结构的一部分,用于存储您打开和关闭activity时不想丢失的所有数据。
ViewModel和保存的实例状态都不是长期存储解决方案,因此不能替代本地存储,例如数据库。相反,您应该使用这些机制暂时存储临时UI状态,并将持久存储用于其他应用程序数据。 有关如何利用本地存储长期保留应用程序模型数据(例如,设备重新启动)的更多详细信息,请参阅Guide to App Architecture。
管理UI状态:分而治之
通过将工作划分到各种类型的持久性机制中,可以有效地保存和恢复UI状态。在大多数情况下,根据数据复杂性、访问速度和生命周期的权衡,每种机制都应该存储不同类型的数据(在activity中使用的):
- 本地持久存储:存储在打开和关闭activity时不想丢失的所有数据。
- 示例:歌曲对象的集合,可包括音频文件和元数据。
-
ViewModel
:在内存中存储显示相关的UI控制器所需的所有数据。- 示例:最近搜索的歌曲对象和最近的搜索查询。
- onSaveInstanceState():存储少量数据,以便在系统停止时轻松重新加载activity状态,然后重新创建UI控制器。与其在这里存储复杂对象,不如将复杂对象持久存储在本地存储中,并在onSaveInstanceState()中存储这些对象的惟一ID。
- 示例:存储最近的搜索查询。
例如,考虑一个允许您搜索歌曲库的activity。以下是处理不同事件的方法:
当用户添加一首歌时,ViewModel
立即委托本地存储该数据。如果这个新添加的歌曲应该显示在UI中,那么还应该更新ViewModel
对象中的数据,以反映添加的歌曲。记住要在主线程之外执行所有数据库插入。
当用户搜索一首歌时,从数据库中为UI控制器加载的任何复杂歌曲数据都应该立即存储在ViewModel对象中。您还应该将搜索查询本身保存在ViewModel对象中。
当activity进入后台时,系统调用onSaveInstanceState()。您应该将搜索查询保存在onSaveInstanceState()包中。这一小部分数据很容易保存。它也是将activity恢复到当前状态所需的所有信息。
恢复复杂状态:重新组装碎片
当用户返回activity时,有两种可能的场景来重新创建activity:
- 该activity在系统停止后被重新创建。该activity将查询保存在onSaveInstanceState() bundle中,并应将查询传递给
ViewModel
。ViewModel
看到它没有缓存搜索结果,并使用给定的搜索查询委托加载搜索结果。 - 该activity是在配置更改之后创建的。该activity将查询保存在onSaveInstanceState() bundle中,并且
ViewModel
已经缓存了搜索结果。您将查询从onSaveInstanceState() bundle传递到ViewModel
,该确定它已经加载了必要的数据,并且不需要重新查询数据库。
注意:最初创建activity时,onSaveInstanceState()
bundle不包含任何数据,ViewModel
对象为空。创建ViewModel
对象时,传递一个空查询,该查询告诉ViewModel
对象尚无加载数据。因此,activity以空状态开始。
其他资源
要了解有关保存UI状态的更多信息,请参阅以下资源。
Blogs
- ViewModels : A Simple Example
- ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders