Compose 横竖屏切换时状态如何保存?rememberSaveable 实现原理分析

Posted fundroid_方卓

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Compose 横竖屏切换时状态如何保存?rememberSaveable 实现原理分析相关的知识,希望对你有一定的参考价值。

前言

这篇文章中提到了 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 展开的:

  1. 通过 key 恢复持久化的数据
  2. 基于 key 注册 ValueProvider,等待合适时机执行数据持久化
  3. 在 onDispose 中被注销注册

registry 是一个 SaveableStateRegistry

恢复 key 的数据

rememberSaveable 是加强版的 remember,首先要具备 remember 的能力,可以看到内部也确实是调用了 remember 来创建数据同时缓存到 Composition 中。init 提供了 remember 数据的首次创建。被创建的数据在后续某个时间点进行持久化,下次执行 rememberSaveable 时会尝试恢复之前持久化的数据。具体过程分为以下两步:

  1. 通过 registry.consumeRestored 查找 key 获取 Saveable,
  2. 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:ListSaverMapSaver,可以用来转换成 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 做的事情类似:

  1. 基于 key 恢复 bundle 数据,
  2. 基于 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 平台完成了横竖屏切换时的状态保存。

最后我们用一个图收尾,红色是保存数据时的数据流流向,绿色是恢复数据时的数据流流向:

以上是关于Compose 横竖屏切换时状态如何保存?rememberSaveable 实现原理分析的主要内容,如果未能解决你的问题,请参考以下文章

面试题:Activity横竖屏切换时的生命周期如何变化

Activity横竖屏切换时 一些数据的保存

Activity横竖屏切换生命周期变化

Swift 支持某个页面横竖屏切换与强制横屏

《夜神安卓模拟器》切换横竖屏方法介绍

android怎么设置横竖屏切换