Compose原理十三之状态保存机制

15 阅读8分钟

一、前言

在 Compose 中,状态管理是核心。我们经常使用 remember 来保存状态,但它无法在 Activity 重建(如屏幕旋转)或进程被杀后存活。为了解决这个问题,Compose 提供了 rememberSaveable。而当我们需要在组件被移出组合树(如 Navigation 切换、LazyColumn 滚动)时依然保留状态,就需要用到 SaveableStateHolder

本文将深入源码,逐行解析这两个核心 API 的工作原理,揭示它们是如何巧妙地将 Compose 的内存状态与安卓系统的 Bundle 机制结合起来的。

二、 rememberSaveable 源码逐行解析

2、1 痛点与使用场景(例子)

假设我们有一个计数器或者输入框。如果只用 remember,当手机发生屏幕旋转时,Activity 会被销毁重建,计数器的值就会归零丢失。 为了让状态能抵抗 Activity 重建(进程被杀也能存活),我们需要把它换成 rememberSaveable

@Composable
fun Counter() {
    // 即使屏幕旋转,count 的值依然会保留!
    var count by rememberSaveable { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

rememberSaveable 的核心作用是:在组件首次组合时初始化状态,并在系统需要保存状态时将数据打包,在系统恢复时将数据解包还原。

2、2 rememberSaveable

我们来看最核心的 rememberSaveable 重载方法(位于 RememberSaveable.kt):

@Composable
public fun <T : Any> rememberSaveable(
    vararg inputs: Any?, // 1. 依赖项数组,当 inputs 改变时,状态会重新初始化
    saver: Saver<T, out Any> = autoSaver(), // 2. 保存器,定义了如何将类型 T 转换为可以存入 Bundle 的类型
    key: String? = null, // 3. 状态的唯一标识键
    init: () -> T, // 4. 状态的初始化工厂函数
): T {
    // 5. 获取当前代码位置在组合树中的唯一哈希值
    val compositeKey = currentCompositeKeyHashCode
    
    // 6. 确定最终使用的 key。如果用户没传,就用系统生成的 compositeKey 转换成 36 进制字符串
    val finalKey =
        if (!key.isNullOrEmpty()) {
            key
        } else {
            compositeKey.toString(MaxSupportedRadix)
        }
    @Suppress("UNCHECKED_CAST") (saver as Saver<T, Any>)

    // 7. 【关键】通过 CompositionLocal 获取当前环境下的 SaveableStateRegistry(状态注册表)
    // 在 Android 中,这个 Registry 最终会连接到 Activity 的 SavedStateRegistry
    val registry = LocalSaveableStateRegistry.current

    // 8. 使用普通的 remember 来缓存一个 SaveableHolder 对象
    val holder = remember {
        // 9. 尝试从 registry 中恢复之前保存的数据
        // registry?.consumeRestored(finalKey) 会取出之前存入的 Bundle 数据
        // saver.restore(it) 会将 Bundle 数据转换回类型 T
        val restored = registry?.consumeRestored(finalKey)?.let { saver.restore(it) }
        
        // 10. 如果恢复成功,就用恢复的值;否则调用 init() 初始化新值
        val finalValue = restored ?: init()
        
        // 11. 创建 SaveableHolder,它负责管理状态的生命周期和保存逻辑
        SaveableHolder(saver, registry, finalKey, finalValue, inputs)
    }

    // 12. 检查 inputs 是否发生变化。如果变了,返回 null,否则返回 holder 中缓存的值
    val value = holder.getValueIfInputsDidntChange(inputs) ?: init()
    
    // 13. 每次重组后,更新 holder 内部的引用(如 saver, registry, value 等)
    SideEffect { holder.update(saver, registry, finalKey, value, inputs) }

    // 14. 返回最终的状态值
    return value
}

2、3 SaveableHolder:状态的守护者

SaveableHolderrememberSaveable 的幕后英雄,它实现了 RememberObserver 接口,从而能够感知自己被加入或移出组合树的生命周期。

private class SaveableHolder<T>(
    private var saver: Saver<T, Any>,
    private var registry: SaveableStateRegistry?,
    private var key: String,
    private var value: T,
    private var inputs: Array<out Any?>,
) : SaverScope, RememberObserver {
    
    // 1. 用于保存注册表返回的凭证,方便后续注销
    private var entry: SaveableStateRegistry.Entry? = null
    
    // 2. 【核心】这是一个高阶函数,当系统需要保存状态时,Registry 会调用它
    // 它内部调用 saver.save(value) 将当前内存中的 value 转换为可保存的格式
    private val valueProvider = {
        with(saver) { save(requireNotNull(value) { "Value should be initialized" }) }
    }

    // ... 省略 update 方法 ...

    // 3. 注册逻辑
    private fun register() {
        val registry = registry
        require(entry == null) { "entry($entry) is not null" }
        if (registry != null) {
            // 检查提供的值是否可以被保存(在 Android 上通常意味着能否放入 Bundle)
            registry.requireCanBeSaved(valueProvider())
            // 向 Registry 注册自己:把 key 和 valueProvider 交给 Registry
            // 这样当系统触发 onSaveInstanceState 时,Registry 就能通过 valueProvider 拿到最新数据
            entry = registry.registerProvider(key, valueProvider)
        }
    }

    // 4. 当组件首次进入组合树时触发
    override fun onRemembered() {
        register() // 挂号登记
    }

    // 5. 当组件被移出组合树时触发(例如 if 条件变为 false,或者 Navigation 切换页面)
    override fun onForgotten() {
        entry?.unregister() // 撤销挂号
    }

    // 6. 当组件被放弃组合时触发
    override fun onAbandoned() {
        entry?.unregister() // 撤销挂号
    }
    
    // ...
}

2、4 为什么组件离开时必须注销(onForgotten)?

你可能会问:既然 rememberSaveable 最终是借助 Activity 的 onSaveInstanceState 来保存数据的,那为什么不在组件离开组合树时保留它的注册信息,而是非要调用 onForgotten 去注销它呢?如果一直留着,不就能自动缓存数据了吗?

原因有两点:

  1. 内存泄漏与状态膨胀: Compose 的界面是动态的。比如一个 LazyColumn 列表有 10000 项,如果你滑动列表,每一项的 rememberSaveable 都不注销,那么系统的 SavedStateRegistry 里就会堆积 10000 个 valueProvider。当 Activity 触发 onSaveInstanceState 时,系统会尝试把这 10000 个状态全部打包进 Bundle。这不仅会导致严重的内存泄漏,还会因为 Bundle 体积过大(Android 限制通常为 1MB 左右)直接抛出 TransactionTooLargeException 导致应用崩溃。
  2. 生命周期语义的正确性: 在 Compose 的设计哲学中,当一个组件被移出组合树(Dispose),就意味着它在逻辑上“死亡”了。一个已经死亡的组件,不应该再向系统提供状态。如果它还需要存活,那说明它的状态应该被提升(State Hoisting)到更高的层级,或者使用专门的机制(如 SaveableStateHolder)来显式地管理这种“离屏存活”的特殊需求。

因此,rememberSaveable 必须在组件离开时注销自己。而对于那些确实需要“离屏存活”的场景(如 Tab 切换、Navigation 回退栈),Compose 提供了 SaveableStateHolder 来专门处理。

2、5 rememberSaveable 的工作流

  1. 初始化/恢复: 尝试从 LocalSaveableStateRegistry 中根据 key 恢复数据。如果没有,则调用 init
  2. 挂号登记: 组件进入屏幕(onRemembered),向 Registry 注册一个 valueProvider
  3. 系统保存: 当 Android 系统触发 onSaveInstanceState 时,Registry 遍历所有注册的 valueProvider,收集数据并存入 Bundle。
  4. 撤销挂号: 当组件离开屏幕(onForgotten),从 Registry 中注销。

致命缺陷: 如果组件因为 Navigation 切换或 LazyColumn 滚动被移出组合树,onForgotten 会被调用,注册被撤销。此时如果系统触发保存,或者你再切回该页面,状态就丢失了!为了解决这个问题,SaveableStateHolder 登场了。

三、 SaveableStateHolder 源码逐行解析

3、1 痛点与使用场景(例子)

上面提到,rememberSaveable 只有在组件还在屏幕上(没被移出组合树)时才有效。如果是被主动移出组合树的场景呢? 比如我们做了一个底部导航栏,切换 Tab:

@Composable
fun BadTabScreen() {
    var currentTab by remember { mutableStateOf("A") }
    
    // 切换 Tab 时,另一个 Tab 的组件会被直接从组合树中移除 (Dispose)
    if (currentTab == "A") {
        var textA by rememberSaveable { mutableStateOf("") }
        TextField(textA, onValueChange = { textA = it })
    } else {
        var textB by rememberSaveable { mutableStateOf("") }
        TextField(textB, onValueChange = { textB = it })
    }
}

问题来了: 你在 Tab A 输入了 "Hello",切到 Tab B,再切回 Tab A,你会发现 "Hello" 丢失了! 为什么?因为切到 Tab B 时,Tab A 的 TextField 被移出了屏幕,它的 rememberSaveable 触发了 onForgotten(),撤销了向系统的挂号登记。数据直接被丢弃。

为了拯救这种“离屏”状态,我们需要请出 SaveableStateHolder

@Composable
fun GoodTabScreen() {
    var currentTab by remember { mutableStateOf("A") }
    // 1. 创建 SaveableStateHolder
    val saveableStateHolder = rememberSaveableStateHolder()
    
    Column {
        // ... 切换按钮代码省略 ...

        // 2. 用 SaveableStateProvider 包裹你的离屏内容,并给它一个唯一的 key(比如 "TabA")
        if (currentTab == "A") {
            saveableStateHolder.SaveableStateProvider(key = "TabA") {
                var textA by rememberSaveable { mutableStateOf("") }
                TextField(textA, onValueChange = { textA = it })
            }
        } else {
            saveableStateHolder.SaveableStateProvider(key = "TabB") {
                var textB by rememberSaveable { mutableStateOf("") }
                TextField(textB, onValueChange = { textB = it })
            }
        }
    }
}

现在,即便你来回切换,输入的文本也不会丢失了!SaveableStateHolder 是怎么做到的?它的设计非常巧妙,相当于在系统 Registry 和组件之间加了一层“代理”或“缓存”。

3、2 rememberSaveableStateHolder()

首先看它是如何被创建的:

@Composable
public fun rememberSaveableStateHolder(): SaveableStateHolder =
    // 1. 注意!它自己就是用 rememberSaveable 保存的!
    // 这意味着 SaveableStateHolderImpl 内部的数据可以抵抗 Activity 重建和进程死亡
    rememberSaveable(saver = SaveableStateHolderImpl.Saver) { 
        SaveableStateHolderImpl() 
    }.apply { 
        // 2. 将系统级的 Registry 传给它,作为它的 parent
        parentSaveableStateRegistry = LocalSaveableStateRegistry.current 
    }

3、3 SaveableStateHolderImpl 的核心结构

private class SaveableStateHolderImpl(
    // 1. 【大肚子字典】用于在内存中暂存那些“不在屏幕上”的组件的状态
    // 它的结构是:Map<组件的Key, Map<状态的Key, 状态值>>
    private val savedStates: MutableMap<Any, Map<String, List<Any?>>> = mutableMapOf()
) : SaveableStateHolder {
    
    // 2. 记录当前正在屏幕上显示的组件的“假 Registry”
    private val registries = mutableScatterMapOf<Any, SaveableStateRegistry>()
    
    var parentSaveableStateRegistry: SaveableStateRegistry? = null
    
    // ...

3、4 SaveableStateProvider:瞒天过海的代理机制

这是最核心的方法,它为每个被包裹的组件提供了一个局部的、假的 Registry。

    @Composable
    override fun SaveableStateProvider(key: Any, content: @Composable () -> Unit) {
        // 1. ReusableContent 确保当 key 改变时,内部的节点可以被复用或重新初始化
        ReusableContent(key) {
            
            // 2. 为当前这个 key(通常代表一个页面或一个列表项)创建一个专属的“假 Registry”
            val registry = remember {
                require(canBeSaved(key)) { ... }
                
                // SaveableStateRegistryWrapper 是一个包装类
                SaveableStateRegistryWrapper(
                    // 初始化时,去大肚子字典 savedStates 里找找有没有之前暂存的旧数据
                    // 如果有,就喂给这个假 Registry,用于恢复状态
                    base = SaveableStateRegistry(restoredValues = savedStates[key], canBeSaved)
                )
            }
            
            // 3. 【偷天换日】利用 CompositionLocal 的就近覆盖原则
            // 把系统级的 LocalSaveableStateRegistry 替换成我们刚刚创建的假 registry!
            // 这样一来,content 里面所有的 rememberSaveable,都会向这个假 registry 挂号登记!
            CompositionLocalProvider(
                LocalSaveableStateRegistry provides registry,
                LocalSavedStateRegistryOwner provides registry,
                content = content,
            )
            
            // 4. 【截胡机制】监听这个 content 的生命周期
            DisposableEffect(Unit) {
                require(key !in registries) { "Key $key was used multiple times " }
                
                // 刚进入屏幕时:
                // 从暂存字典中移除旧数据(因为数据已经被假 registry 消费了)
                savedStates -= key
                // 记录当前正在活跃的假 registry
                registries[key] = registry
                
                // 当 content 即将被移出屏幕时(例如切走了 Tab):
                onDispose {
                    if (registries.remove(key) === registry) {
                        // 【全场最核心代码】
                        // 赶在 content 内部的 rememberSaveable 执行 onForgotten 撤销挂号之前,
                        // 强制调用假 registry 的 performSave() 搜刮所有状态!
                        // 并把搜刮到的数据存入大肚子字典 savedStates 中暂存!
                        registry.saveTo(savedStates, key)
                    }
                }
            }
        }
    }

registry.saveTo 的实现:

    private fun SaveableStateRegistry.saveTo(
        map: MutableMap<Any, Map<String, List<Any?>>>,
        key: Any,
    ) {
        // 调用假 Registry 的 performSave(),它会遍历所有向它挂号的 valueProvider,拿到最新数据
        val savedData = performSave()
        if (savedData.isEmpty()) {
            map -= key
        } else {
            // 将搜刮到的数据存入大肚子字典
            map[key] = savedData
        }
    }

3、5 应对进程死亡:Saver 的实现

如果组件不在屏幕上,它的数据存在 savedStates 内存里。如果此时进程被杀,内存数据怎么保全? 还记得 rememberSaveableStateHolder 是用 rememberSaveable 包裹的吗?当系统触发保存时,会调用它的 Saver:

    companion object {
        val Saver: Saver<SaveableStateHolderImpl, *> =
            Saver(
                // 保存时:调用 saveAll()
                save = { it.saveAll() }, 
                // 恢复时:把系统传回来的 Map 重新塞给 SaveableStateHolderImpl
                restore = { SaveableStateHolderImpl(it) }
            )
    }

    private fun saveAll(): MutableMap<Any, Map<String, List<Any?>>>? {
        // 1. 拿到装满离线组件数据的大肚子字典
        val map = savedStates
        // 2. 遍历当前还在屏幕上的活跃组件,强制搜刮它们的数据也放进 map 里
        registries.forEach { key, registry -> registry.saveTo(map, key) }
        // 3. 返回这个超级大 Map,它最终会被系统塞进 Bundle 里!
        return map.ifEmpty { null }
    }

3、6 removeState 方法的原理

    override fun removeState(key: Any) {
        // 1. 尝试从活跃的 registries 中移除
        if (registries.remove(key) == null) {
            // 2. 如果不在活跃列表中,说明它在离线暂存区,从 savedStates 中彻底删除
            savedStates -= key
        }
    }

为什么需要 removeState 当一个页面被彻底销毁(例如从 Navigation 的回退栈中 pop 出去),我们不再需要它的状态了。如果不调用 removeState,它的状态会一直残留在 savedStates 字典中,导致内存泄漏。Navigation 框架在内部会在合适的时机调用这个方法来清理无用状态。

3、7 总结

  1. 正常显示: SaveableStateProvider 创建假 Registry,组件内的 rememberSaveable 向假 Registry 注册。
  2. 切走页面(离树): DisposableEffect.onDispose 触发,假 Registry 强制搜刮数据,存入 Holder 的 savedStates 内存字典。组件随后销毁,撤销注册。
  3. 切回页面(回树): SaveableStateProvider 重新执行,从 savedStates 取出旧数据喂给新的假 Registry。组件内的 rememberSaveable 从假 Registry 恢复数据。
  4. 进程被杀: 系统触发保存,Holder 的 Saver.saveAll() 被调用,将 savedStates 和当前活跃 Registry 的数据合并,上交给系统的真 Registry,最终存入 Android 的 Bundle
  5. 进程重启: 系统将 Bundle 还原,Holder 的 Saver.restore 被调用,savedStates 满血复活,等待页面切回时恢复数据。