保存 UI 状态
在因内存不足被系统杀死的应用或者因为 Configuration changes 导致的Activity重建中,以及时的方式保存和恢复Activity的UI状态是用户体验的关键部分。在这种情况下用户是希望新启动的Activity 和最后销毁的 Activity之间的状态要保持一致,但是如果我们不加以处理,系统并不会自动恢复这些数据。
为了实现用户的期望,我们可以单独或者组合使用 ViewModel、onSaceInstanceState()方法或者是一个本地文件存储的方式来保存 UI 状态,至于是组合还是单独用这三种手段,要取决于 UI 数据的复杂度、运行设备的情况、恢复数据的速度和消耗内存的大小来综合考虑。
不管我们使用哪种方式,我们应该让用户看到他们最后看到的 UI 状态,用一种自然平滑的方式来加载数据,避免占用太多时间加载需要重新呈现的 UI 状态,特别是在频繁的发生 Configuration changes 的情况,比如旋转屏幕。大多数情况下我们应该使用 ViewModel 和 onSaveInstanceState()方法。
本章讨论我们用来满足期望所使用的几种方式之间的差别,并列举不同的使用场景,在不同的场景中我们将讨论这几种方式的优缺点,并权衡地选出一种较为合理的手段。
用户期望和系统行为
用户可能希望清空 UI 状态,也可能期望保存 UI 状态,这取决于用户的操作。在某些情况中系统的行为结果和用户的期望是一致的,在另外一些情况则不满足用户的期望。
用户主动让 UI 状态消失
用户期望当启动一个 Activity 之后,一些不重要的 UI 状态也可以一直存在,除非他们自己手动关闭一个 Activity,用户可以通过以下几种方式关闭一个 Activity:
- 按返回按钮
- 后台滑掉应用
- 按返回导航栈中上一个 Activity 的按钮
- 在设置中心中强制停止一个应用
- 操作了某些执行结束操作的动作,比如某些动作所调用的
Activity.finish()
方法
用户觉得通过以上的方式后,应该是会销毁所有临时的 UI 状态的,如果重新打开 Activity 以后,它的界面应该是一个初始的、干净的界面。在这些场景中,系统的行为结果就满足用户的期望---Activity 实例会被销毁并从内存中移除,并且任何的 UI 状态都不会被保存,在重新打开也就不会恢复这些 UI 状态了。
对于某些场景来说这样的操作后用户并不希望彻底不保存 UI 状态,比如对于一个浏览器而言,用户期望的可能是点击返回按钮之后不销毁这个界面而是返回上一页浏览的网页。
系统让 UI 状态消失
用户希望在Configuration changes,比如旋转屏幕、拖入多窗口模式之后,Activity 的 UI 状态应该和之前的保持一致。但是系统的默认行为结果和此相反,任何的 UI 状态都不会得到保存和恢复,虽然我们可以通过更改 Configuration changes 的默认行为,但是我们并不推荐这种方式。
用户希望在他们短暂地离开了 Activity 之后再回来还能看到的 Activity 还是最后看到的样子,比如用户在我们的搜索 Activity 中输入搜索内容之后按了 home 键或者被一个来电打断了---当他们重新回到这个界面的时候,用户希望能看到他们最后输入的搜索词。
在这种场景中,我们的 app 应该在后台做一些操作让我们的应用能在内存中驻留。但是系统可能因为内存不足而杀死我们的不可视应用,在这种情况下,Activity 对象会被销毁,所以用户重新加载 app 之后看到的不是他们最后看到的状态,这种情况是用户不希望看到的。
保存UI状态几种方式的介绍
当用户期望UI状态得以保存而系统默认的行为是不保存的时候,我们必须手动存储和恢复丢失的数据,确保用户感知不到Activity被销毁重建了。
保存UI状态大致分为三类,它们之间保存数据的方式和时效各不相同,如下图所示:
使用ViewModel来保存因Configuration changes所丢失的数据
ViewModel中保存的数据不会因为Configuration changes发生而丢失,并且由于其中的数据是保存在内存中的,所以恢复数据的速度是几种方式中最快的。
ViewModel是与Activity(或者其他LifecycleOwner对象)关联的,在configuration changes之后,ViewModel对象依然在内存中存活,并且自动和新重建的Activity(或者其他LifecycleOwner对象)建立关联。
当我们调用finish()
方法的时候,这意味着用户希望清除Activity的状态,这时ViewModel对象会被系统自动销毁。
不像saved instance state在应用、Activity被系统杀死、自动重建还能存活,在这个过程中ViewModel对象会被销毁。所以这也是我们应该使用ViewModel配合onSaveInstanceState()或者其它硬盘存储手段来保存UI状态的原因,我们可以在onSaveInstanceState()中保存数据的关键标志,在系统杀死、重建应用之后去恢复数据。
使用onSaveInstanceState()备份数据,在系统杀死、重建应用之后恢复
在系统杀死一个应用(Activity)后,如果稍后这个应用(Activity)又被系统自动启动,那么该Activity的onSaveInstanceState()会被调用,我们可以在这个方法中恢复Activity中的UI状态。
虽然通过saved instance state方式保存的Bundle类型数据,可以在Configuration changes和系统杀死、自动重建的情况下存活,但是这种方式会受到硬盘大小和读写速度所限制,除此之外,需要保存的数据需要先序列化,序列化数据的开销也受到数据大小的限制,因为序列化过程是发生在主线程的,所以如果我们需要序列化大量数据,这时候可能会导致程序丢帧,对用户体验造成一定的影响。
所以不要用onSaveInstanceState()去保存大量数据,比如图片或者需要消耗大量序列化、反序列化时间的复杂结构数据。相反的我们应该只保存一些基础、简单的数据比如String。同样的我们应该保存最关键的少量必要数据,比如一个ID,当其它恢复策略时效后,我们可以通过这个ID去恢复之前的UI状态。
onSaveInstanceState()不是必须实现的。举个例子,在浏览器中,当用户点击返回按钮的时候,我们可能让浏览器显示用户上一个访问的网页,这个时候我们就没必要使用onSaveInstanceState()方法,而是通过其它手段将上一个网页的所有数据保存在本地。
除此之外,当你使用Intent打开一个Activity的时候,如果需要传递数据,我们使用Intent.putExtras
方法来传递数据,当被启动的Activity因Configuration changes导致销毁重建的时候,我们仍然可以从Intent处获取extras数据,这可以代替onSaveInstanceState()方法。
但是ViewModel是无可替代的,在任何情况下我们都需要使用ViewModel保存数据,避免在Configuration changes情况下浪费资源去从数据库加载数据。
如果要保存的UI数据简单且轻量级,我们可以单独使用onSaveInstanceState()来保存状态数据。
使用硬盘存储来复杂、大量的数据
通过将数据保存在本地,比如数据库或者偏好设置文件,只要用户不卸载或者清理app的数据,我们的数据都不会丢失。所以应用因为什么原因重新打开,我们都可以从本地文件中恢复之前的数据。但是这种方式的开销比前面两种大,因为我们需要将硬盘中的数据先读取、载入内存。通常我们设置一个APP的时候,数据库存储已经成为架构中的一个重要环节,它用来保存用户所有想要保存的数据,并且在下次打开应用的时候还可以恢复。
在这种情况下,ViewModel和saved instance state这两种方式保存的时间都不够长,所以这种情况下本地化存储方案是无法替代的,比如使用数据库存储。相反的,对于临时数据,我们不应该使用本地化存储方案,而应该使用ViewModel或者saved instance state方案。
使用分治法存储UI状态
将'保存和恢复'任务分配给这几种方式,让它们协和完成这个过程是一种高效的手段。在大多数情况下,鉴于需要保存数据的复杂性、几种方式的生命周期和获取数据的速度,我们应该让它们存储不同类型的数据。
- 本地文件存储:当我们重复开闭一个Activity,我们不希望某些重要数据丢失,这时候我们就需要用到这种存储方案。
比如存储音乐文件。 - ViewModel:存储临时的、和Activity关联的数据,数据可以是复杂的,因为数据存储在内存中。
比如存储最近播放的音乐和最近搜索的关键字 - onSaveInstanceState(): 存储少量、简单、必须的数据,当系统杀死、自动重建Activity以后用来恢复数据。如果是复杂的数据,那么应该讲数据存储在本地文件比如数据库,然后在这个方法中存储数据的标志符,在Activity重建后通过这个标志符去数据库中加载、恢复UI状态。
比如存储最近搜索的关键字
举个例子,当我们的Activity允许我们在音乐库搜索音乐的时候,以下是处理不同事件的方法:
当用户添加一首音乐的时候,ViewModel会通知本地存储组件去存储这首音乐。如果这首新添加的音乐需要呈现在UI中,我们还应该更新ViewModel对象中的数据,反应它关联的UI状态。请记住,我们应该在子线程对数据库进行操作。
当用户搜索一首歌的时候,从数据库读取出来的歌曲不管多么复杂, 我们都应该马上存储在ViewModel中,同时将这个搜索关键字也一并保存在ViewModel对象中。
当应用进入后台,系统会调用onSaveInstanceState()。我们应该保存搜索关键字在onSaveInstanceState()的bundle参数中,这个小数据容易保存,这个数据也是我们恢复UI状态所需的所有信息,轻量又全面。
使用组装法恢复复杂数据
当用户返回Activity时,有两种可能的场景会重新创建Activity:
该Activity在被系统停止后被重新创建。我们可以获取之前保存在onSaveInstanceState()中的查询关键字,我们应该将这个关键字传递给ViewModel对象,ViewModel发现内存中并没有存储对应的歌曲,所以它通知本地数据层去加载对应的歌曲,最后将结果反馈给UI层。
当Configuration changes之后,Activity被重新加载,我们可以获取之前保存在onSaveInstanceState()中的查询关键字,我们应该将这个关键字传递给ViewModel对象,因为ViewModel可以在Configuration changes后存活,所以它可以在内存中找到对应的歌曲信息,并直接将结果反馈给UI层。这意味着它不需要从数据库中重新查询。