安卓现代化开发系列——从状态保存到SavedState

由于安卓已经诞生快二十载,其最初的开发思想与现代的开发思想已经大相径庭,特别是Jetpack库诞生之后,项目中存在着新老思想混杂的情况,让许多的新手老手都措手不及,项目大步向屎山迈进。为了解决这个问题,开发者必须弄懂新旧两种开发模式,这就是《安卓现代化开发系列》诞生的意义,本系列并不会包含隐晦难懂的代码,一切的文字都是以理解本质为主,起到一个抛钻引玉的作用。

1、为什么需要状态保存?

说「状态保存」之前,我们先讲一讲为什么需要状态保存:

常见的window、linux系统不同的是,移动端的操作系统拥有的内存更少,因此这类系统更容易面临内存不足的情况,如何最大限度利用较少的内存是移动端操作系统比较重要的问题。

对于安卓系统来说,一个Activity不可见时,即这时已经跳转到了另外一个Activity或者整个App都处于处于后台的情况下,同时它的生命周期处于「Stoped」。在这之后,一旦出现内存不足的情况,Android系统就会考虑销毁这些用户不可见的Activity,这样就可以释放它们占用的内存,给予用户目前正在交互的Activity更多的内存,避免彻底的OOM(out of momory)异常出现。

此刻就出现了一个问题,如果只是单纯的把Activity销毁了,那么之前用户操作的信息就全部丢失了,可以想象的一个场景是:用户正在编辑一段日记的时候,来了一个电话,当通话结束之后(假设此刻处于后台的编辑日记的Activity由于内存不足被销毁了),那么返回到App的时候,用户会发现花了很多时间编辑的日记已经全部丢失,这样的App逻辑是无法接受的。因此我们需要一种机制:在即将被销毁的时候保存Activity的状态,页面重建之后根据之前保存的状态恢复页面,这种“机制”就是标题所谓的「状态保存」。

2、状态在安卓中意味着什么

在安卓中,当我们提到「状态保存」的时候,开发者保存的状态其实就是某些「成员变量」。

因此,读者可以简单的理解为,当一个变量存在于View中,即此变量为View的成员变量时,此变量可能会由于View的重建而丢失,因为View此时是一个全新的实例。同理,当Activity与Fragment也会存在类似的场景丢失他们的成员变量。因此开发者需要处理这些可能会由于实例的替换导致丢失成员变量的场景,这个处理的过程就是安卓的「状态保存」。

下面结合代码理解一下:

2.1、View的实例状态

安卓现代化开发系列——从状态保存到SavedState_第1张图片

根据上文所述View中的那些成员变量就是「View的实例状态」,这里展示一个按钮案例,常见的按钮就有”选中“和”未选中“两个状态,因此开发者会用一个布尔值来存储这个状态,但是由于重建机制的存在,View会被一个新的实例代替,那么此时的View就丢失了状态了。

2.2、Activity的实例状态

安卓现代化开发系列——从状态保存到SavedState_第2张图片

一个Activity中存在着View,在View的内部存在着「View的实例状态」,同时它旁边也存在着一些Activity的成员变量,这些成员变量和View内部的状态共同组成了「Activity的实例状态」。

同样,当遇到重建的场景时,Activity会同时丢失自身的状态与View内部的实例状态(在View没有实现状态保存的情况下)。

2.3、Fragment的实例状态

安卓现代化开发系列——从状态保存到SavedState_第3张图片

Activity几乎类似,Fragment也同样存在着View与自身的成员变量,因此「View的实例状态」与这些成员变量共同组成了「Fragment的实例状态」。

需要注意的是:由于Fragment的特殊性,Fragment的生命周期与FragmentView的生命周期是不一致的,一个Fragment在自身的生命周期内可能会跨越多个View的重建,这也导致了Fragment的状态保存分裂为「成员变量的保存」与「View的实例状态的保存」,这两者在Activity中是同时发生的,而Fragment中并不一定同时。

2.4、实例状态的包含关系

由于View是依附于组件中的,因此「组件的实例状态」除了组件本身的变量,还包括了「View的实例状态」,因此当我们说组件的状态保存的时候,其实还包括了保存View的状态。

也许读者此时会联想到,Fragment也可以存在于父Fragment或者父Activity,那么它们之间的实例状态也是包含关系吗?

答案是对的,当Activity保存自身状态的时候,同时也会让它所包含的Fragment保存实例状态。

下面这张图可以展示状态关系:

安卓现代化开发系列——从状态保存到SavedState_第4张图片

3、图示状态保存与恢复

下面援引自 The Real Best Practices to Save 的几张图可以很好阐述状态保存时发生的事情:

当Activity需要保存实例状态的时候,它会先遍历所有的View让他们各自保存自己的状态,然后打包放在自己的实例状态中的某个地方,和自身的其他业务状态保存在一起。

安卓现代化开发系列——从状态保存到SavedState_第5张图片

相反,当Activity需要恢复状态的时候,它会从实例状态中找出所有View之前保存的状态,然后将他们恢复给所有的View,同时恢复自身的业务状态。

安卓现代化开发系列——从状态保存到SavedState_第6张图片

对于Fragment来说整个过程是类似的,这里就不展示了。

4、实操状态保存与恢复

4.1、View

View的状态保存与恢复核心方法是onSaveInstanceState()onRestoreInstanceState()

开发者只需要为当前的ViewonSaveInstanceState()onRestoreInstanceState()中实现图中的操作即可。

安卓现代化开发系列——从状态保存到SavedState_第7张图片

需要注意的是:

View能够实现状态保存与恢复的前提是:必须在UI树中存在唯一的ID。

换句话说,这要求了开发者必须在布局的xml中为该View赋予唯一的ID,或者动态添加的时候生成一个唯一ID。

这并不难理解,状态保存的本质是将状态缓存在某个容器中,需要恢复的时候从容器中取出来,而ID则是取的Key,如果没有Key那又如何保存状态呢?

4.2、Activity

Activity的状态保存与View类似,也是一对onSaveInstanceState()onRestoreInstanceState()方法,但是开发者大多数选择在onCreate()中恢复状态,这取决于实际的需要。

安卓现代化开发系列——从状态保存到SavedState_第8张图片

4.3、Fragment

Fragment的状态保存也与Activity类似,下面直接看图即可:

安卓现代化开发系列——从状态保存到SavedState_第9张图片

依然需要注意的是:在2.3中提到,由于Fragment的实例与UI的分离的设计模式,因此会发生只保存UI状态的情况,因此上图中的onSaveInstanceState()是不会调用的,我们从方法名中也可以看出这是保存实例状态。

5、状态保存与恢复的时机?

5.1、Activity

Activity被意外销毁时,需要保存状态,并在Activity重新恢复显示时恢复状态。

对于Activity来说,除了用户手动从当前Activity退出以外(这种情况无需状态保存),还有以下两种情况会导致Activity会被系统销毁:

  1. 配置发生变化(用户修改了手机的语言、暗夜模式等)。
  2. Activity处于「停止」状态时因系统限制(内存不足)而被销毁。

为什么用户主动按下返回按钮导致Activity销毁不需要状态保存而后两种情况需要状态保存呢?

主要的原因是前者是**「用户意料之内的行为」,而后两种情况属于「用户意料之外的行为」**。当一个用户旋转一个页面时,亦或者用户从页面A跳转到B,并稍后从B返回到A时,这两种情况用户并不希望页面的信息丢失了,否则就会出现上文出现的「编辑一半的日记被来电清空」的特殊情况,这对于用户来说是不可以接受的。

下面结合一张图来展示Activity生命周期与状态保存的关系:

安卓现代化开发系列——从状态保存到SavedState_第10张图片

由图中可见,Activity的状态保存与恢复发生在onSaveInstanceState()onRestoreInstanceState()中,具体的细节下文会解释,这里读者记住它发生的时机即可。(在安卓9之后,保存状态发生在onStop()之后,这与安卓9之前的版本有细微的差异)。

5.2、Fragment

Fragment保存状态的时机相对复杂,有好几种情况。同时保存业务状态和保存View的状态的时机并不一定是一致的。

下面援引官方文档的一句结论:

注意:仅当 fragment 的宿主 activity 调用自己的 onSaveInstanceState(Bundle) 时,系统才会调用 onSaveInstanceState(Bundle)

实际上这个结论并不能完全概括Fragment保存状态的所有时机,只是阐述了其中一种由Activity发生状态保存的时候,顺便保存其子Fragment状态的情况,而Fragment保存状态的情况还有两种,笔者下文会讲,这里先从Activity发生状态保存时开始讲起。

5.2.1、Activity状态保存时,顺便保存Fragment的状态,恢复状态同理

这种情况属于自动发生的情况,上文讲Activity的状态保存时提到,Activity的实例状态其实也包含了Fragment的实例状态,因此Activity保存状态中也包含了Fragment的状态。

通过图示来理解这个过程:

安卓现代化开发系列——从状态保存到SavedState_第11张图片

这种情况就是官方文档中提到的「宿主Activity」调用FragmentonSaveInstanceState()的时候保存状态的情况。

5.2.2、主动保存与恢复Fragment的状态

有时候Fragment并不一定要跟随Activity进行状态保存,在Activity的生命周期期间,其内部的Fragment也会主动保存与恢复状态,这暗示着这些Fragment存在着需要销毁实例的情况。

下面我们讲讲如何主动保存与恢复这些Fragment的状态:

首先我们看FragmentManager(1.5.5版本)的源码,其中存在着一个saveFragmentInstanceState(Fragment)的方法,它是public的因此开发者可以使用这个方法「主动保存一个Fragment的状态」,随后就可以抛弃掉这个Fragment实例。

安卓现代化开发系列——从状态保存到SavedState_第12张图片

还可以看到,保存的状态为SavedState,随后我们可以根据这个状态去为新创建的Fragment实例恢复刚才的状态。

那么我们应该如何为新创建的Fragment实例恢复呢?Fragment专门为这种情况提供了一个方法:

安卓现代化开发系列——从状态保存到SavedState_第13张图片

可以看到这个方法单纯就是为了初始化一个Fragment的状态,唯一需要注意的是:

setInitialSavedState(SavedState)只能在FragmentFragmentManager纳入管理之前调用。

这一对API的意义是什么呢?目的只有一个就是节省内存,因为开发者经常会遇到这种场景:

Fragment暂时不可见,希望回收它的实例但是保存状态,稍后新建一个类型相同的Fragment实例,然后用刚才保存的状态将「新建的实例」恢复成「旧的实例」的状态。

如果你熟悉ViewPager2,那么你一定了解它的一个offscreenPageLimit的机制,FragmentStateAdapter这个适配器会将那些离视窗范围太远的Fragment销毁掉,这个场景就是上述的提到的。那么它又是如何在重新回到被销毁的Fragment的位置的时候将其状态恢复的呢?

答案就是上文提到的主动恢复状态的方法:setInitialSavedState(SavedState)

安卓现代化开发系列——从状态保存到SavedState_第14张图片

虽然旧的Fragment实例被销毁了,但是ViewPager2通过保存它的状态的方式,稍后使用了一个新的Fragment与之前保存的状态恢复到了当初的样子。

虽然开发者很少会实现自己的「跨Fragment实例的状态保存恢复机制」,但是理解其本质有利于理解ViewPager等框架的基础原理。

5.2.3、Fragment进入回退栈的时候

下面引用一段来自 Fragment Lifecycle in Android - GeeksforGeeks 的图阐述一下Fragment特殊的生命周期:

安卓现代化开发系列——从状态保存到SavedState_第15张图片

在图中可以看出,从onCreate()onDestroy()的生命周期中,Fragment还可能进入一个从onCreateView()onDestroyView()的循环,这个循环的次数可能是大于1次的。

换句话说,在Fragment实例被创建到被销毁的期间,它的View也许会经历1次或以上的重新创建。

那么什么情况下会发生「只销毁View而不销毁实例」的情况呢?答案如标题所述,就是Fragment进入回退栈的时候。

安卓现代化开发系列——从状态保存到SavedState_第16张图片

当开发者使用FragmentManager执行replace操作并调用了addToBackStack()的时候,意味着「使用了一个新的Fragment替换掉了旧的Fragment」,但是这个操作是可逆的,因为操作添加进了回退栈,也就意味着,用户按返回键的时候,会返回到之前那个被替换的Fragment

这意味着,旧的Fragment只是暂时被压到了一个栈中,待会仍然可以通过退栈的方式重新回到用户的屏幕中,这个旧的Fragment会经历onPause()->onStop()->onDestroyView()的过程,但是仅此而已。它的实例没有被销毁,只是View被销毁了而已。

但是开发者仍然不需要担心View被销毁后,View中的实例状态丢失了,因为Fragment考虑到了这种情况,在FragmentManager检测到这种场景的时候,会主动让Fragment保存其contentView的状态并存放在FragmentManager中。

然而对于开发者来说,并不需要额外花心思在如何处理FragmentcontentView的状态如何被保存,因为这个本质是属于View层面的东西,了解这个机制的含义更多是解决一些开发中的隐性问题:

Fragment的实例和UI的生命周期实则是分离的,不能将两者等同,例如不能简单的使用Fragment的生命周期回调对UI进行一些操作而是使用其contentView的生命周期,否则将会发生越界访问(UI销毁了仍尝试访问)或者内存泄漏。

Fragment的UI初始化应该写在onCreateView()中而不是onCreate(),这样能避免在生命周期期间发生UI销毁,导致UI没有被重新初始化等问题。

6、古法状态保存的问题

上述分点讨论了ViewActivityFragment的古法状态保存,不知道读者是否感觉到了他们有一些设计上的缺陷呢?笔者这里总结了几点:

我们以Activity为例,回顾一下它的状态保存:

安卓现代化开发系列——从状态保存到SavedState_第17张图片

6.1、不同类型的状态之间混合在一起

如果我们将一个页面中不同业务的状态,都通过同一种方式(key-value)全部缓存在onSaveInstanceState(Bundle)提供的bundle中,那么维护起来将十分的复杂。

6.2、上层主动保存状态而不是状态持有者本身

另外还要注意的是:Activity本质上需要在自身的基础上,通过重写方法的方式来保存和恢复一些状态,然后提供这些状态给别的组件使用。

这样的问题是:状态的使用者往往不是Activity而是Activity的一些附属的组件,例如一些成员变量、View(这里的情况下把View的状态上升到了Activity来维护,也是开发中常见的一种方式)等。

如果全部的状态都通过Activity亲自来维护和恢复,如果后续需要保存的状态多起来的话,将会为Activity的开发提高了很大的负担。再者这也是违背单一权责的,单个方法中需要管理的不同业务的状态太多了。

6.3、缺乏统一的管理层

Activity、Dialog、Fragment等不同场景均依赖自身的方法,缺乏统一的代码。数据的维护可能需要团队的代码规范。

6.4、总结

古法状态保存由于历史的原因,设计的缺陷非常的大,开发者很难在复杂的业务中精准、高效地保存页面状态。

7、走向SavedState库

谷歌为了解决上述的状态保存的问题,提出了SavedState库。

让我们看看「SavedState」库的整体脉络:

安卓现代化开发系列——从状态保存到SavedState_第18张图片

图中可以看出,状态是缓存在SavedStateRegistry中的,而该Registry又通过不同的SavedStateProvider来保存不同类型的状态,达到了分类管理的效果。

单纯一幅图是没法完全理解这个库的,下面进行分点讲解:

7.1、关键类解析

7.1.1、SavedStateRegistryOwner

安卓现代化开发系列——从状态保存到SavedState_第19张图片

可以看到,SavedStateRegistryOwner是一个非常简单的接口,它的目的是对外提供一个SavedStateRegistry,这是一个集中管理状态的管理器,下文会提到这里略过。

还需要注意的是,该接口是LifecycleOwner的子类,因此它拥有感知生命周期的能力。不难理解,毕竟需要状态保存与恢复需要发生在组件恰当的生命周期中。

7.1.2、SavedStateProvider

安卓现代化开发系列——从状态保存到SavedState_第20张图片

SavedStateProvider是一个接口,它的含义是「状态提供器」,实现该接口的类本质上就是定义了如何保存一类状态的方式。当需要保存状态时,SavedStateRegistry就会让它管理的所有Provider按定义保存所有状态。

7.1.3、SavedStateRegistry

SavedStateRegistry是一个管理器,管理着一系列的SavedStateProvider,当其拥有者重建时,Registry也会重新创建一个新的实例。当Registry的拥有者(例如Activity)被创建的时候,Registry就会尝试恢复之前保存的状态。

让我们总体概览一下SavedStateRegistry的代码, 不用为复杂的代码感到担心,下面会详解:

安卓现代化开发系列——从状态保存到SavedState_第21张图片

7.1.3.1、注册与反注册「状态提供者」

上文中提到,SavedStateRegistry是一个管理一系列SavedStateProvider的容器,因此它提供了一对方法用于注册和解绑这些StateProvider,稍后这些Provider在保存与恢复状态中起到了关键作用。

安卓现代化开发系列——从状态保存到SavedState_第22张图片

7.1.3.2、恢复与消费状态

SavedStateRegistry分别提供了performRestore(Bundle?)consumeRestoredStateForKey(String?)来实现状态的恢复与消费。

安卓现代化开发系列——从状态保存到SavedState_第23张图片

从代码中可见,「恢复状态」只是从外部的Bundle中抓取一部分存放到Registry内部,并没有去执行取值的操作。如果需要从恢复后的状态中取值,则再次多次调用consumeRestoredStateForKey(String?)来取状态。

那么为什么「恢复状态」之后还要「消费状态」呢?

这里笔者的结论是:存在「恢复状态后,还不能立即消费状态」的场景。因此谷歌在设计该Api的时候,把状态的消费单独分离出来,适配更多场景。

需要注意的是:消费状态必须要在状态保存发生之后,可以使用SavedStateRegistry.isRestored来判断,否则会异常。

7.1.3.3、保存状态

保存状态的代码也非常简洁,就是将SavedStateRegistry中的所有SavedStateProvider集中打包放到外部的bundle中。

安卓现代化开发系列——从状态保存到SavedState_第24张图片

7.1.4、SavedStateRegistryController

这个类并没有什么特殊的,他只是SavedStateRegistry之间的包装类,结合SavedStateRegistryOwner做了一些生命周期上的工作,本质还是使用performRestore(Bundle?)performSave(Bundle?)两个方法与Registry进行沟通:

安卓现代化开发系列——从状态保存到SavedState_第25张图片

因为Controller多了与生命周期的监听,因此实际开发中,直接使用SavedStateRegistry还是比较少的,大多数使用SavedStateRegistryController来间接控制。

7.1.5、总结

让我们重新回到这张图中,根据刚才的解析总结一下各组件的功能:

安卓现代化开发系列——从状态保存到SavedState_第26张图片

  • SavedStateRegistryOwner:SavedStateRegistry的提供者。
  • StateRegistryController:间接控制SavedStateRegistry
  • SavedStateRegistry:状态的管理者。
  • SavedStateProvider:状态的提供者。

7.3、谷歌眼中的SavedState

我们结合谷歌提供的AndroidX代码来理解一下SavedState库是如何被使用的。

7.3.1、ComponentDialog

ComponentDialog中,存在着SavedState库的核心代码,我们抽取出来看看:

安卓现代化开发系列——从状态保存到SavedState_第27张图片

可见,该Dialog实现了SavedStateRegistryOwner接口,因此它可以对外提供SavedStateRegistry,上文中提到,由于SavedStateController包含的能力更多,因此都是直接使用SavedStateController来间接操控SavedStateRegistry

onSaveInstanceState()中,使用了Controller来保存状态,而在onCreate(Bundle?)方法中,使用了Controller来恢复状态。

还有一点值得注意的是,在initViewTreeOwners()方法中,将当前的SavedStateRegistryOwner绑定在了Dialog所在的DecorView中,这样给该Dialog下面的所有View提供了访问该DialogSavedStateRegistry的能力。

关于ViewTreeStateRegistryOwner的设计,在生命周期那一章已经简单阐述过类似的概念,不懂的读者可以回去阅读生命周期章。

7.3.2、ComponentActivity

同时我们再看看ComponentActivity,本质上和Dialog也相似,关键代码已经全部截取出来了,读者结合Dialog的代码自行领会即可。

Fragment的几乎也一样,这里就不展示了。

安卓现代化开发系列——从状态保存到SavedState_第28张图片

8、SavedState的最佳实践——SavedStateHandle

那么开发中如何使用SavedState呢,实际上开发者并不需要在项目中亲自试用SavedState,例如在Activity中直接使用SavedStateRegistry,而是配合ViewModel与其配套的SavedStateHandle一起使用。

为什么会这样呢?因为直接在Activity、Fragment中声明变量已经不适合现代mvvm等开发模式了,而是将状态和逻辑写在ViewModel中,而ActivityFragment等只做数据的订阅载体。

因此ViewModel就需要一种访问其组件上的缓存的状态的能力,这里就引出本篇文章的主角——SavedStateHandle,我们直接先看看它是如何被使用的吧:

安卓现代化开发系列——从状态保存到SavedState_第29张图片

只需要在ViewModel的构造函数中添加SavedStateHandle这个参数即可,开发者通过SavedStateHandle可以读取到ActivitygetIntent()的值,亦或者是读取到FragmentgetArguments()的值。相反的,也可以通过SavedStateHandle往里面写入值。

这种用法有什么用呢?作用是两点:

  1. 读取Activity或者Fragment的入参。
  2. 写入与读取状态,这些状态可以被状态保存机制保存起来。

第一点就不细说了,这个可以让ViewModel读取到当前所在组件的入参,做一些逻辑上的初始化工作。

这里重点是第二点,如果你了解ViewModel,那么你肯定知道ViewModel在配置更新导致的组件重建的时候,是不会销毁的,然而一旦遇到非配置更新导致的重建的情况(例如处于Stoped状态的Activity由于内存不足被系统回收),ViewModel就会被销毁了。

为了解决ViewModel在上述情况被销毁导致状态丢失的问题,开发者可以通过SavedStateHandle来写入和读取一些值,这个值会在发生状态保存的时候被写入到组件的Bundle中,并在组件组件重建的时候重新回到SavedStateHandle中,这让ViewModel拥有了读写状态的能力。

8.1、SavedStateHandle如何做到的

也许你一定很好奇SavedStateHandle是如何能够与Activity或者Fragment发生联系的,如果上述关于ComponentActivity等代码你还有印象,那么你肯定能猜到:

SavedStateHandle访问了组件的SavedStateRegistry,并在上面读取和写了状态。

让我们通过代码来看看SavedStateHandle做了什么事:

首先,ViewModel的构建都是通过工厂类反射得到的,因此我们使用了一个带参的ViewModel,那么必定存在一个对应的工厂类,这个工厂类在SavedState库中已经实现好了:

安卓现代化开发系列——从状态保存到SavedState_第30张图片

可以看到,这个工厂类在构建方法中允许传入一个外部的SavedStateRegistryOwner来获取其SavedStateRegistry,同时还传入了一个defaultArgs,还记得上面说的吗?这个参数在Activity中是getIntent().getExtras(),在Fragment中是getArguments()。我们直接在ComponentActivity的代码中验证下:

安卓现代化开发系列——从状态保存到SavedState_第31张图片

验证成功,关于Fragment读者可以亲自验证下,同样是getDefaultViewModelProviderFactory()方法。

综上我们可以得出以下结论:

  1. ViewModel默认可以使用带SaveStateHandle的参数的构造函数,因为工厂方法已经默认提供了。
  2. SavedStateHandleViewModel提供了读取组件入参、读取写入状态的能力。
  3. SavedStateHandle的能力的基础源自工厂类拥有组件的SaveStateRegistry,因此SavedStateHandle被构建时同时也传入了组件的SaveStateRegistry

下面用一张图简单描述一下它们的关系:

安卓现代化开发系列——从状态保存到SavedState_第32张图片

8.2、状态保存的思路转变

如果都通过ViewModel来保存业务中的状态,那么View又如何保存呢,毕竟View是没法直接访问ViewModel的,其实这陷入了一种思维的误区。

进入MVVM时代之后,开发者更聚焦于状态本身,通过改变状态来让UI自动发生响应,因此View本身的状态可以「上升」到ViewModel中,组件发生销毁之后,ViewModel仍可以安全的保存状态,因此重新走一遍订阅状态的流程又可以让View恢复状态了。

因此,并不是View不保存状态了,而是保存的位置迁移到了ViewModel

这里用一张新的图来阐述这种变化:

安卓现代化开发系列——从状态保存到SavedState_第33张图片

9、引入SavedStateHandle后,状态保存走向何方?

上文中提到,在引入MVVM开发思想以及对应的实现工具ViewModel之后,我们应该在ViewModel中结合SavedStateHandle来实现状态保存,但我们需要保存ViewModel中所有的属性吗?答案是不必要。

首先,基于ViewModel的视角去看一看SavedStateHandle

安卓现代化开发系列——从状态保存到SavedState_第34张图片

可以看到ViewModel存在着两种类型的属性:

  • 由SavedStateHandle直接管理的、ViewModel实例销毁时不会丢失的属性
  • 直接编码在ViewModel自身的临时变量

刚才提到,虽然开发者可以将一切变量都通过SavedStateHandle保存在状态中,避免ViewModel销毁后丢失,但是这是不必要的,为什么呢?下面从一个实际场景出发来解释下:

假设页面A是一个列表,页面入参是用户的ID,从网络中加载用户相关的推荐房间数据。

使用者进入到页面后,页面开始加载数据,同时使用者也在页面中勾选了一些筛选之类的选项。

紧接着使用者收到了来电,APP进入了后台,页面也随即进入Stoped状态。

不久之后,用户没有返回APP,而是使用了其他的APP,这导致了手机内存不足,原来的列表页面被系统回收

这个时候我们就要开始考虑哪些是需要保存的状态了:

  • 对于入参ID,我们可以得知,所有入参是存在于Intent().getExtra()中的,在开发过程中不用担心这块数据因组件以外销毁而丢失。
  • 对于筛选选项之类的,笔者认为这一块是需要保存的,最好使用SavedStateHandle处理一下。
  • 对于加载的列表,笔者认为这一块是不需要保存的,如果长时间没有回来APP,即时性比较强的列表数据其实是没有恢复的必要,重新为用户加载一份并不是特别糟糕的体验。(注:个人观点,仍需要结合实际开发场景)

下面用代码来复现上述这种场景:

安卓现代化开发系列——从状态保存到SavedState_第35张图片

代码很多,但是并不复杂,我们分别从ActivityViewModel两部分讲解:

Activity

定义了一个uid的Key值以及一个伴生方法,用于其他页面跳转到当前Activity时在Intent的Extras中附带一个用户id的参数;

添加了一个生命周期观察器,用于在进入Created时,调用ViewModel的方法去抓取数据。

ViewModel

使用savedStateHandle去读取Activity的Intent里面的Extra,用来获取用户id。

定义了一个userFilter的Key值,用于通过savedStateHandle去读写Activity的Intent,用于避免开启筛选的状态在重建组件时丢失;

定义了dataList用于缓存根据用户id请求的结果,但是此结果并不会保存在Intent中,因此会在组件因内存不足被系统销毁时丢失。

最后笔者还是要说一句,决定哪些数据需要保存哪些数据可以放弃,这个要视乎实际项目的需要。

10、总结

本章中,我们从最古早的方法回调的方式了解如何保存与恢复状态,发现出许多旧版方式存在的缺陷,然后从SavedState库着手,以一种新的方式完成状态保存。可以看出近些年来谷歌在努力着手解决安卓整体框架的缺陷。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

你可能感兴趣的:(Android,移动开发,APP,android,移动开发,framework,安卓,ui)