Compose状态保存rememberSaveable原理解析
作者:fundroid 发布时间:2021-12-26 10:07:56
前言
我曾经在一篇介绍 Compose Navigation 的文章 中提到了 Navigation 的状态保存实际是由 rememberSaveable
实现的,有同学反馈希望单独介绍一下 rememberSaveable 的功能及实现原理。
我们都知道 remember 可以保存数据、避免状态因重组而丢失,但它依然无法避免在 ConfigurationChanged 时的数据丢失。想要在横竖屏切换等场景下依然保存状态,就需要使用 rememberSavable。
从一个报错说起
首先,在代码使用上 rememberSaveable 和 remember 没有区别:
//保存列表状态
val list = rememberSaveable {
mutableListOf<String>()
}
//保存普通状态
var value by rememberSaveable {
mutableStateOf("")
}
如上,只要将 remember 改为 rememberSaveable,我们创建的状态就可以跨越横竖屏切换甚至跨越进程持续保存了。不过 rememberSaveable 中并非任何类型的值都可以存储:
data class User(
val name: String = ""
)
val user = rememberSaveable {
User()
}
上面代码运行时会发生错误:
java.lang.IllegalArgumentException: User(name=) cannot be saved using the current SaveableStateRegistry. The default implementation only supports types which can be stored inside the Bundle. Please consider implementing a custom Saver for this class and pass it to rememberSaveable().
User 无法存入 Bundle。这非常合理,因为 rememberSaveable 中数据的持久化最终在 ComponentActivity#onSaveInstanceState
中执行,这需要借助到 Bundle 。
rememberSaveable 源码分析
那么,rememberSaveable 是如何关联到 onSaveInstanceState 的呢?接下来简单分析一下内部实现
@Composable
fun <T : Any> rememberSaveable(
vararg inputs: Any?,
saver: Saver<T, out Any> = autoSaver(),
key: String? = null,
init: () -> T
): T {
//...
// 通过 CompositionLocal 获取 SaveableStateRegistry
val registry = LocalSaveableStateRegistry.current
// 通过 init 获取需要保存的数据
val value = remember(*inputs) {
// registry 根据 key 恢复数据,恢复的数据是一个 Saveable
val restored = registry?.consumeRestored(finalKey)?.let {
// 使用 Saver 将 Saveable 转换为业务类型
saver.restore(it)
}
restored ?: init()
}
// 用一个 MutableState 保存 Saver,主要是借助 State 的事务功能避免一致性问题发生
val saverHolder = remember { mutableStateOf(saver) }
saverHolder.value = saver
if (registry != null) {
DisposableEffect(registry, finalKey, value) {
//ValueProvider:通过 Saver#save 存储数据
val valueProvider = {
with(saverHolder.value) { SaverScope { registry.canBeSaved(it) }.save(value) }
}
//试探数值是否可被保存
registry.requireCanBeSaved(valueProvider())
//将ValueProvider 注册到 registry ,等到合适的时机被调用
val entry = registry.registerProvider(finalKey, valueProvider)
onDispose {
entry.unregister()
}
}
}
return value
}
如上,逻辑很清晰,主要是围绕 registry
展开的:
通过 key 恢复持久化的数据
基于 key 注册 ValueProvider,等待合适时机执行数据持久化
在 onDispose 中被注销注册
registry 是一个 SaveableStateRegistry
。
恢复 key 的数据
rememberSaveable 是加强版的 remember,首先要具备 remember 的能力,可以看到内部也确实是调用了 remember 来创建数据同时缓存到 Composition 中。init
提供了 remember 数据的首次创建。被创建的数据在后续某个时间点进行持久化,下次执行 rememberSaveable 时会尝试恢复之前持久化的数据。具体过程分为以下两步:
通过 registry.consumeRestored 查找 key 获取 Saveable,
Saveable 经由 saver.restore 转换为业务类型。
上述过程涉及到两个角色:
SaveableStateRegistry:通过 CompositionLocal 获取,它负责将 Bundle 中的数据反序列化后,返回一个 Saveable
Saver:Saver 默认有 autoSaver 创建,负责 Saveable 与业务数据之间的转换。
Saveable 并不是一个在具体类型,它可以是可被持久化(写入 Bundle)的任意类型。对于 autoSaver
来说, 这个 Saveable 就是业务数据类型本身。
private val AutoSaver = Saver<Any?, Any>(
save = { it },
restore = { it }
)
对于一些复杂的业务结构体,有时并非是所有字段都需要持久化。Saver 为我们提供了这样一个机会机会,可以按照需要将业务类型转化为可序列化类型。Compose 也提供了两个预置的 Saver:ListSaver
和 MapSaver
,可以用来转换成 List 或者 Map。
关于恢复数据的 Key :可以看到数据的保存和恢复都依赖一个 key,按道理 key 需要在保存和恢复时严格保持一致 ,但我们平日调用 rememberSaveable 时并没有指定具体的 key,那么在横竖屏切换甚至进程重启后是如何恢复数据的呢?其实这个 key 是 Compose 自动帮我们设置的,它就是编译期插桩生成的基于代码位置的 key ,所以可以保证每次进程执行到此处都保持不变
注册 ValueProvider
SaveableStateRegistry 在 DisposableEffect 中关联 key 注册 ValueProvider
。 ValueProvider 是一个 lambda,内部会调用 Saver#save
将业务数据转化为 Saveable。
Saver#save 是 SaverScope 的扩展函数,所以这里需要创建一个 SaverScope 来调用 save 方法。SaverScope 主要用来提供 canBeSaved 方法,我们在自定义 Saver 时可以用来检查类型是否可被持久化
ValueProvider 创建好后紧接着会调用 registry.registerProvider
进行注册,等待合适的时机(比如 Activity 的 onSaveInstanceState)被调用。在注册之前,先调用 requireCanBeSaved
判断数据类型是否可以保存,这也就是文章前面报错的地方。先 mark 一下,稍后我们看一下具体检查的实现。
注销 registry
最后在 onDispose 中调用 unregister 注销之前的注册 。
rememberSaveable 的基本流程理清楚了,可以看见主角就是 registry,因此有必要深入 SaveableStateRegistry 去看一下。我们顺着 LocalSaveableStateRegistry
可以很容易找到 registry 的出处。
DisposableSavableStateRegistry 源码分析
override fun setContent(content: @Composable () -> Unit) {
//...
ProvideAndroidCompositionLocals(owner, content)
//...
}
@Composable
@OptIn(ExperimentalComposeUiApi::class)
internal fun ProvideAndroidCompositionLocals(
owner: AndroidComposeView,
content: @Composable () -> Unit
) {
val view = owner
val context = view.context
//...
val viewTreeOwners = owner.viewTreeOwners ?: throw IllegalStateException(
"Called when the ViewTreeOwnersAvailability is not yet in Available state"
)
val saveableStateRegistry = remember {
DisposableSaveableStateRegistry(view, viewTreeOwners.savedStateRegistryOwner)
}
//...
CompositionLocalProvider(
//...
LocalSaveableStateRegistry provides saveableStateRegistry,
//...
) {
ProvideCommonCompositionLocals(
owner = owner,
//...
content = content
)
}
}
如上,我们在 Activity 的 setContent 中设置各种 CompositionLocal,其中就有 LocalSaveableStateRegistry,所以 registry 不仅是一个 SaveableStateRegistry,更是一个 DisposableSaveableStateRegistry 。
接下来看一下 DisposableSaveableStateRegistry 的创建过程 。
saveableStateRegistry 与 SavedStateRegistry
注意下面这个 DisposableSaveableStateRegistry 不是真正的构造函数,它是同名构造函数的一个 Wrapper,在调用构造函数创建实例之前,先调用 androidxRegistry
进行了一系列处理:
internal fun DisposableSaveableStateRegistry(
id: String,
savedStateRegistryOwner: SavedStateRegistryOwner
): DisposableSaveableStateRegistry {
//基于 id 创建 key
val key = "${SaveableStateRegistry::class.java.simpleName}:$id"
// 基于 key 获取 bundle 数据
val androidxRegistry = savedStateRegistryOwner.savedStateRegistry
val bundle = androidxRegistry.consumeRestoredStateForKey(key)
val restored: Map<String, List<Any?>>? = bundle?.toMap()
// 创建 saveableStateRegistry,传入 restored 以及 canBeSaved
val saveableStateRegistry = SaveableStateRegistry(restored) {
canBeSavedToBundle(it)
}
val registered = try {
androidxRegistry.registerSavedStateProvider(key) {
//调用 register#performSave 并且转为 Bundle
saveableStateRegistry.performSave().toBundle()
}
true
} catch (ignore: IllegalArgumentException) {
false
}
return DisposableSaveableStateRegistry(saveableStateRegistry) {
if (registered) {
androidxRegistry.unregisterSavedStateProvider(key)
}
}
}
androidxRigistry 跟 rememberSaveable 中的 registry 做的事情类似:
基于 key 恢复 bundle 数据,
基于 key 注册 SavedStateProvider。
但 androidxRegistry 不是一个 SaveableStateRegistry 而是一个 SavedStateRegistry
。名字上有点绕,后者来自 androidx.savedstate
,属于平台代码,而 SaveableStateRegistry 属于 compose-runtime 的平台无关代码。可见这个构造函数的同名 Wrapper 很重要,他就像一个桥梁,解耦和关联了平台相关和平台无关代码。
DisposableSaveableStateRegistry 与 SaveableStateRegistryImpl
DisposableSaveableStateRegistry 真正的构造函数定义如下:
internal class DisposableSaveableStateRegistry(
saveableStateRegistry: SaveableStateRegistry,
private val onDispose: () -> Unit
) : SaveableStateRegistry by saveableStateRegistry {
fun dispose() {
onDispose()
}
}
这里用了参数 saveableStateRegistry 作为 SaveableStateRegistry 接口的代理。saveableStateRegistry 实际是一个 SaveableStateRegistryImpl
对象,它像这样创建:
val saveableStateRegistry = SaveableStateRegistry(restored) {
canBeSavedToBundle(it)
}
fun SaveableStateRegistry(
restoredValues: Map<String, List<Any?>>?,
canBeSaved: (Any) -> Boolean
): SaveableStateRegistry = SaveableStateRegistryImpl(restoredValues, canBeSaved)
SaveableStateRegistryImpl 被创建时传入两个参数:
restoredValues:androidxRegistry 恢复的 bundle 数据,是一个 Map 对象。
canBeSaved : 用来检查数据是否可持久化,可以的看到这里实际调用了 canBeSavedToBundle。
canBeSavedToBundle
文章开头的报错就是 requireCanBeSaved -> canBeSavedToBundle
检查出来的,通过 canBeSavedToBundle 看一下 rememberSaveable 支持的持久化类型:
private fun canBeSavedToBundle(value: Any): Boolean {
// SnapshotMutableStateImpl is Parcelable, but we do extra checks
if (value is SnapshotMutableState<*>) {
if (value.policy === neverEqualPolicy<Any?>() ||
value.policy === structuralEqualityPolicy<Any?>() ||
value.policy === referentialEqualityPolicy<Any?>()
) {
val stateValue = value.value
return if (stateValue == null) true else canBeSavedToBundle(stateValue)
} else {
return false
}
}
for (cl in AcceptableClasses) {
if (cl.isInstance(value)) {
return true
}
}
return false
}
private val AcceptableClasses = arrayOf(
Serializable::class.java,
Parcelable::class.java,
String::class.java,
SparseArray::class.java,
Binder::class.java,
Size::class.java,
SizeF::class.java
)
首先, SnapshotMutableState
允许被持久化,因为我们需要在 rememberSaveable 中调用 mutableStateOf;其次,SnapshotMutableState 的泛型必须是 AcceptableClasses
中的类型,我们自定义的 User 显然不符合要求,因此报了开头的错误。
SaveableStateRegistryImpl 源码分析
前面理清了几个 Registry 类型的关系,整理如下图
SaveableStateRegistry 接口的各主要方法都由 SaveableStateRegistryImpl 代理的:
consumeRestored:根据 key 恢复数据
registerProvider:注册 ValueProvider
canBeSaved:用来检查数据是否是可保存类型
performSave:执行数据保存
canBeSaved 前面介绍过,其实会回调 canBeSavedToBundle。接下来看一下 SaveableStateRegistryImpl 中其他几个方法是如何实现的:
consumeRestored
override fun consumeRestored(key: String): Any? {
val list = restored.remove(key)
return if (list != null && list.isNotEmpty()) {
if (list.size > 1) {
restored[key] = list.subList(1, list.size)
}
list[0]
} else {
null
}
}
我们知道 restored
是从 Bundle 中恢复的数据,实际是一个 Map了类型。而 consumeRestored
就是在 restored 中通过 key 查找数据。restore 的 Value 是 List 类型。当恢复数据时,只保留最后一个只。顺便吐槽一下 consumeRestored 这个名字,将 restore 这个 private 成员信息暴露给了外面,有些莫名其妙。
registerProvider
override fun registerProvider(key: String, valueProvider: () -> Any?): Entry {
require(key.isNotBlank()) { "Registered key is empty or blank" }
@Suppress("UNCHECKED_CAST")
valueProviders.getOrPut(key) { mutableListOf() }.add(valueProvider)
return object : Entry {
override fun unregister() {
val list = valueProviders.remove(key)
list?.remove(valueProvider)
if (list != null && list.isNotEmpty()) {
// if there are other providers for this key return list back to the map
valueProviders[key] = list
}
}
}
}
将 ValueProvider 注册到 valueProviders ,valueProviders 也是一个值为 List 的 Map,同一个 Key 可以对应多个 Value。返回的 Entry 用于 onDispose 中调用 unregister。
DisposableSaveableStateRegistry 是一个 CompositionLocal 单例,所以需要 unregister 避免不必要的泄露。注意这里要确保同一个 key 中的 List 中的其它值不被移除
不解:什么情况下同一个 key 会 registerProvider 多个值呢?
performSave
override fun performSave(): Map<String, List<Any?>> {
val map = restored.toMutableMap()
valueProviders.forEach { (key, list) ->
if (list.size == 1) {
val value = list[0].invoke()
if (value != null) {
check(canBeSaved(value))
map[key] = arrayListOf<Any?>(value)
}
} else {
map[key] = List(list.size) { index ->
val value = list[index].invoke()
if (value != null) {
check(canBeSaved(value))
}
value
}
}
}
return map
}
在这里调用了 ValueProvider 获取数据后存入 restored ,这里也是有针对 Value 是 List 类型的特别处理。performSave 的调用时机前面已经出现了,是 androidxRegistry 注册的 Provider 中调用:
androidxRegistry.registerSavedStateProvider(key) {
//调用 register#performSave 并且转为 Bundle
saveableStateRegistry.performSave().toBundle()
}
SavedStateProvider 会在 onSaveInstance 时被执行。
至此, rememberSaveable 持久化发生的时机与平台进行了关联。
最后回看 androidxRegistry
最后我们再回看一下 DisposableSavableStateRegistry,主要是使用 androidxRegistry 获取 key 对应的数据,并注册 key 对应的 Provider。那么 androidxRegistry 和 key 是怎么来的?
internal fun DisposableSaveableStateRegistry(
id: String,
savedStateRegistryOwner: SavedStateRegistryOwner
): DisposableSaveableStateRegistry {
val key = "${SaveableStateRegistry::class.java.simpleName}:$id"
val androidxRegistry = savedStateRegistryOwner.savedStateRegistry
//...
}
先说 key 。key 由 id 唯一决定,而这个 id 其实是 ComposeView
的 layoutId。我们知道 ComposeView 是 Activity/Fragment 承载 Composable 的容器,rememberSaveable 会按照 ComposeView 为单位来持久化数据。
因为你 ComposeView 的 id 决定了 rememberSaveable 存储数据的位置,如果 Activity/Fragment 范围内如果有多个 ComposeView 使用了同一个 id,则只有第一个 ComposeView 能正常恢复数据,这一点要特别注意
再看一下 androidxRegistry,他由 SavedStateRegistryOwner 提供,而这个 owner 是ComposeView 被 attach 到 Activity 时赋的值,就是 Activity 本身:
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ContextAware,
LifecycleOwner,
ViewModelStoreOwner,
HasDefaultViewModelProviderFactory,
SavedStateRegistryOwner, // ComponentActivity 是一个 SavedStateRegistryOwner
OnBackPressedDispatcherOwner,
ActivityResultRegistryOwner,
ActivityResultCaller {
//...
public final SavedStateRegistry getSavedStateRegistry() {
return mSavedStateRegistryController.getSavedStateRegistry();
}
//...
}
mSavedStateRegistryController
会在 Activity 重建时 onCreate 中调用 performRestore
;在 onSaveInstanceState 时执行 performSave
。
protected void onCreate(@Nullable Bundle savedInstanceState) {
mSavedStateRegistryController.performRestore(savedInstanceState);
//...
}
protected void onSaveInstanceState(@NonNull Bundle outState) {
//...
mSavedStateRegistryController.performSave(outState);
}
mSavedStateRegistryController 最终调用到 SavedStateRegistry 的同名方法,看一下 SavedStateRegistry#performSave
:
fun performSave(outBundle: Bundle) {
//...
val it: Iterator<Map.Entry<String, SavedStateProvider>> =
this.components.iteratorWithAdditions()
while (it.hasNext()) {
val (key, value) = it.next()
components.putBundle(key, value.saveState())
}
if (!components.isEmpty) {
outBundle.putBundle(SAVED_COMPONENTS_KEY, components)
}
}
components 是注册 SavedStateProvider 的 Map。 performSave 中调用 Provider 的 saveState 方法获取到 rememberSaveable 中保存的 bundle,然后存入 outBundle 进行持久化。
至此,rememberSaveable 在 Android 平台 完成了横竖屏切换时的状态保存。
最后我们用一个图收尾,红色是保存数据时的数据流流向,绿色是恢复数据时的数据流流向:
来源:https://juejin.cn/post/7166043043651387406
猜你喜欢
- 介绍Java中的享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享尽可能多的对象来减少内存占用和提高性能.Ja
- 本文实例讲述了java简单列出文件夹下所有文件的方法。分享给大家供大家参考,具体如下:import Java.io.*;public cla
- 本文以实例形式介绍了基于Java实现的Dijkstra算法,相信对于读者研究学习数据结构域算法有一定的帮助。Dijkstra提出按各顶点与源
- 前言本问主要介绍DataBinding在Android App中的使用方法。数据绑定是将“提供器”的数据源与“消费者”绑定并使其同步的一种通
- 由Lombok的@AllArgsConstructor注解引发的错误需求:在Service实现中写了一个方法调用第三方接口同步数据。 功能代
- 线程可以划分优先级,优先级高的线程得到的CPU资源比较多,也就是CPU优先执行优先级高的线程对象中的任务。设置线程优先级有助于帮助线程规划器
- C#Windows server2016服务器搭建NFS共享文件夹与C#上传图片到共享文件夹nfs共享文件夹实现步骤基于:Windows s
- 本文实例为大家分享了java + dom4j.jar提取xml文档内容的具体代码,供大家参考,具体内容如下资源下载页:点击下载本例程主要借助
- 前言泛型,一个孤独的守门者。大家可能会有疑问,我为什么叫做泛型是一个守门者。这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可
- 最近要做一个网站,要求实现验证码程序,经过不断调试,终于成功实现功能。一、验证码生成类生成验证码的话需要用到java的Graphics类库,
- 一个线程如何知道另一线程已经结束?Thread类提供了回答此问题的方法。有两种方法可以判定一个线程是否结束。第一,可以在线程中调用isAli
- 1.1、获取http请求参数是一种刚需我想有的小伙伴肯定有过获取http请求的需要,比如想前置获取参数,统计请求数据做服务的接口签名校验敏感
- 网络基础知识1、OSI分层模型和TCP/IP分层模型的对应关系这里对于7层模型不展开来讲,只选择跟这次系列主题相关的知识点介绍。2、七层模型
- ThreadGroup的作用及方法ThreadGroup线程组,java对这个类的描述呢就是“线程组表示一组线程。此外,线程组还可以包括其他
- 需求基于MTK8163 8.1平台定制导航栏部分,在左边增加音量减,右边增加音量加思路需求开始做之前,一定要研读SystemUI Navig
- 接收JSON浏览器传来的参数,可以是 key/value 形式的,也可以是一个 JSON 字符串。在 Jsp/Servlet 中,我们接收
- SpringBoot项目经常将连接数据库的密码明文放在配置文件里,安全性就比较低一些,尤其在一些企业对安全性要求很高,因此我们就考虑如何对密
- 本文实例讲述了C#清除WebBrowser中Cookie缓存的方法。分享给大家供大家参考,具体如下:最近用C#写一个程序,用一个窗体中的We
- Java 异步实现的几种方式1. jdk1.8之前的Futurejdk并发包里的Future代表了未来的某个结果,当我们向线程池中提交任务的
- 概述从今天开始, 小白我将带大家开启 Java 数据结构 & 算法的新篇章.优先队列优先队列 (Priority Queue) 和队