理解retain{}的内部机制:Jetpack Compose中基于作用域的状态保存

本文译自「Understanding retain{} internals: A Scope-based State Preservation in Jetpack Compose」,原文链接proandroiddev.com/understandi...,由Jaewoong Eum发布于2025年10月15日。

Jetpack Compose 是一种现代 Android UI 开发方式,拥有声明式方法和强大的状态管理原语。虽然 remember{} 能够很好地在重组过程中保存状态,但它有一个根本性的局限性:它无法在配置更改或导航转换后继续生效。引入 retain{},这是一种在瞬时销毁场景下保存状态的新方法,同时保持可组合性。

在本文中,你将深入了解 retain{}RetainScopeRetainObserverRetainedEffect 的内部机制,探索它们的底层工作原理,以及使 Retention 既内存安全又性能卓越的优化方法。

如果你尚未阅读以下之前的文章,理解以下部分将对你有所帮助:

导入 Compose 运行时 retain 库

你可以使用以下依赖项导入 Compose 运行时 retain 库:

kotlin 复制代码
implementation("androidx.compose.runtime:runtime-retain:1.10.0-alpha05")

请注意,此库目前处于实验阶段,未来将不断完善和稳定发布。

理解核心问题:为什么 remember{} 还不够

从本质上讲,remember{} 可以在单个组合生命周期内跨重组保留状态。但是,当你的 Activity 因配置更改而重新创建时会发生什么?或者当导航目标从返回堆栈中移除时?状态会丢失,你的可组合项会重新开始。

传统的 Android 解决方案涉及 ViewModelSavedStateHandle,但它们本身也具有复杂性:序列化要求、手动状态恢复以及在可组合项层次结构之外管理状态的认知开销。如果我们可以拥有一个像 remember{} 一样工作,但又能在这些瞬时销毁后继续存在的东西,那会怎样?

kotlin 复制代码
// With remember: state lost on configuration change
@Composable
fun VideoPlayer() {
    val player = remember { MediaPlayer() } // Lost on rotation!
}

// With retain: state preserved across configuration changes
@Composable
fun VideoPlayer() {
    val player = retain { MediaPlayer() } // Survives rotation!
}

retain{} 背后的关键洞察是,状态销毁并不总是永久性的。通常,它是暂时的;内容会被重新创建,恢复之前的状态会对我们有利。这就是"RetainScope"发挥作用的地方。

retain{} API 及其生命周期

如果你检查 retain 函数签名,会发现它与 remember API 类似,但存在以下区别:

kotlin 复制代码
@Composable
public inline fun <reified T> retain(noinline calculation: () -> T): T

@Composable
public inline fun <reified T> retain(vararg keys: Any?, noinline calculation: () -> T): T

带有 reified 类型参数的 inline 修饰符无需显式类参数即可实现类型安全的保留。但真正的魔力在于其实现:

kotlin 复制代码
@Composable
private fun <T> retainImpl(key: RetainKeys, calculation: () -> T): T {
    val retainScope = LocalRetainScope.current
    val holder = remember(key) {
        val retainedValue = retainScope.getExitedValueOrDefault(key, RetainScopeMissingValue)
        if (retainedValue !== RetainScopeMissingValue) {
            RetainedValueHolder(
                key = key,
                value = @Suppress("UNCHECKED_CAST") (retainedValue as T),
                owner = retainScope,
                isNewlyRetained = false,
            )
        } else {
            RetainedValueHolder(
                key = key,
                value = calculation(),
                owner = retainScope,
                isNewlyRetained = true,
            )
        }
    }

    if (holder.owner !== retainScope) {
        SideEffect { holder.readoptUnder(retainScope) }
    }
    return holder.value
}

此实现揭示了保留的两个阶段特性:

阶段 1:记忆阶段retain{} 首先使用 remember{} 创建一个 RetainedValueHolder。该持有者包装实际值并跟踪其生命周期。

阶段 2:保留阶段 。当持有者离开组合时,RetainScope 不会立即将其丢弃,而是决定是否将其保留以备将来恢复。

RetainedValueHolder 和 RetainScope

首先,如果你仔细研究 RetainedValueHolder 的内部代码,你会发现它是生命周期管理发生的地方。它实现了 RememberObserver 接口,以便与 Compose 的生命周期挂钩:

kotlin 复制代码
internal class RetainedValueHolder<out T> internal constructor(
    val key: Any,
    val value: T,
    owner: RetainScope,
    private var isNewlyRetained: Boolean,
) : RememberObserver {

    override fun onRemembered() {
        if (value is RetainObserver) {
            if (isNewlyRetained) {
                isNewlyRetained = false
                value.onRetained()
            }
            value.onEnteredComposition()
        }
    }

    override fun onForgotten() {
        if (owner.isKeepingExitedValues) {
            owner.saveExitingValue(key, value)
        }

        if (value is RetainObserver) {
            value.onExitedComposition()
            if (!owner.isKeepingExitedValues) value.onRetired()
        }
    }

    override fun onAbandoned() {
        if (owner.isKeepingExitedValues) {
            if (value is RetainObserver) value.onRetained()
            owner.saveExitingValue(key, value)
        } else if (value is RetainObserver) {
            value.onUnused()
        }
    }
}

重要的一点是,该持有者会拦截组合生命周期事件并将其转换为保留事件。当 onForgotten() 被调用时(值离开组合),它会检查 RetainScope 是否保留了已退出的值。如果是,它会保存该值以供将来恢复,而不是丢弃它。

RetainScope 是保留系统的核心概念。它管理何时保留值、如何存储它们以及何时恢复或退出它们。让我们来看看它的细节:

kotlin 复制代码
public abstract class RetainScope : RetainStateProvider {
    protected var keepExitedValuesRequests: Int = 0
        private set

    final override val isKeepingExitedValues: Boolean
        get() = keepExitedValuesRequests > 0

    public abstract fun getExitedValueOrDefault(key: Any, defaultIfAbsent: Any?): Any?
    protected abstract fun saveExitingValue(key: Any, value: Any?)
    protected abstract fun onStartKeepingExitedValues()
    protected abstract fun onStopKeepingExitedValues()
}

作用域有两种运行模式:

  1. 普通模式 (isKeepingExitedValues = false):值离开组合时会被丢弃,就像 remember{} 一样。
  2. 保留模式 (isKeepingExitedValues = true):值退出时会被保留,以便将来恢复。

那么,保留生命周期是如何运作的呢?通过原始源代码中嵌入的图表可以最好地理解保留生命周期:

mipsasm 复制代码
┌──────────────────────┐
│                      │
│ retain(keys) { ... } │
│        ┌────────────┐│
└────────┤  value: T  ├┘
         └──┬─────────┘
            │   ▲
        Exit│   │Enter
 composition│   │composition
   or change│   │
        keys│   │                         ┌──────────────────────────┐
            │   ├───No retained value─────┤   calculation: () -> T   │
            │   │   or different keys     └──────────────────────────┘
            │   │                         ┌──────────────────────────┐
            │   └───Re-enter composition──┤    Local RetainScope     │
            │       with the same keys    └─────────────────┬────────┘
            │                                           ▲   │
            │                      ┌─Yes────────────────┘   │ value not
            │                      │                        │ restored and
            │   .──────────────────┴──────────────────.     │ scope stops
            └─▶(   RetainScope.isKeepingExitedValues   )    │ keeping exited
                `──────────────────┬──────────────────'     │ values
                                   │                        ▼
                                   │      ┌──────────────────────────┐
                                   └─No──▶│     value is retired     │
                                          └──────────────────────────┘

此流程揭示了几个关键的见解:

  1. 保留是有条件的 :只有当 isKeepingExitedValuestrue 时,值才会被保留。
  2. 键很重要:即使在保留期间,更改的键也会导致立即退出。
  3. 恢复是自动的:当内容使用相同的键重新输入时,保留的值将被恢复。
  4. 退出是最终的:保留停止时未恢复的值将被退出。

ControlledRetainScope:可变实现

虽然 RetainScope 提供了抽象,但 ControlledRetainScope 才是主要的实现:

kotlin 复制代码
public class ControlledRetainScope : RetainScope() {
    private val keptExitedValues = SafeMultiValueMap<Any, Any?>()

    override fun saveExitingValue(key: Any, value: Any?) {
        keptExitedValues.add(key, value)
    }

    @Suppress("UNCHECKED_CAST")
    override fun getExitedValueOrDefault(key: Any, defaultIfAbsent: Any?): Any? {
        return keptExitedValues.removeLast(key, defaultIfAbsent)
    }

    override fun onStopKeepingExitedValues() {
        keptExitedValues.forEachValue { value ->
            if (value is RetainObserver) value.onRetired()
        }
        keptExitedValues.clear()
    }
}

该实现使用 SafeMultiValueMap 进行存储是一个重要的设计选择。为什么?因为多个 retain 调用可以具有相同的键:

kotlin 复制代码
@Composable
fun Example() {
    // Both have the same positional key!
    val value1 = retain { "First" }
    val value2 = retain { "Second" }
}

该映射按 LIFO(后进先出)顺序存储每个键的值。恢复时,removeLast() 确保值按其存储的相反顺序恢复,从而保持 retain 调用与其值之间的正确配对。

此外,ControlledRetainScope 支持嵌套在父级 RetainStateProvider 下:

kotlin 复制代码
public fun setParentRetainStateProvider(parent: RetainStateProvider) {
    val oldParent = parentScope
    parentScope = parent

    parent.addRetainStateObserver(parentObserver)
    oldParent.removeRetainStateObserver(parentObserver)

    if (parent.isKeepingExitedValues) startKeepingExitedValues()
    if (oldParent.isKeepingExitedValues) stopKeepingExitedValues()
}

这支持有序的层次结构,其中子作用域从父级继承保留状态。你可以利用此功能使所有 RetainScopes 在配置更改期间保留,根作用域会检测到配置更改,并沿层次结构向下级联保留。

RetainObserver:保留对象的生命周期回调

需要了解其保留生命周期的对象需要实现 RetainObserver

kotlin 复制代码
@Suppress("CallbackName")
public interface RetainObserver {
    public fun onRetained()

    // Successfully retained
    public fun onEnteredComposition() // Entered composition
    public fun onExitedComposition()  // Exited composition
    public fun onRetired()

    // No longer retained
    public fun onUnused()

    // Created but never used
}

这些回调实现了 remember{} 无法实现的资源管理模式。设想一个媒体播放器,当屏幕未显示时应该暂停,但如果可能再次显示,则不应释放资源:

kotlin 复制代码
class RetainableMediaPlayer : RetainObserver {
    private var player: MediaPlayer? = null

    override fun onRetained() {
        player = MediaPlayer()
    }

    override fun onEnteredComposition() {
        player?.play()
    }

    override fun onExitedComposition() {
        player?.pause() // Just pause, don't release
    }

    override fun onRetired() {
        player?.release() // Now we can release
        player = null
    }

    override fun onUnused() {
        // Never entered composition, clean up
        player?.release()
        player = null
    }
}

还应记住,回调遵循严格的顺序保证。当多个 RetainObserver 同时进入组合状态时,它们的 onEnteredComposition 回调将按顺序触发。退出时,onExitedComposition 将按相反顺序触发**,**以确保正确清理嵌套资源。

RememberObserver 限制

你可以在 RetainedValueHolder 中找到一个有趣的安全检查:

kotlin 复制代码
init {
    if (value is RememberObserver && value !is RetainObserver) {
        throw IllegalArgumentException(
            "Retained a value that implements RememberObserver but not RetainObserver. " +
            "To receive the correct callbacks, the retained value '$value' must also " +
            "implement RetainObserver."
        )
    }
}

为什么有这个限制?RememberObserver 回调(onRememberedonForgottenonAbandoned)与保留语义不一致。暂时脱离组合的保留值不会被"遗忘",而是处于不确定状态,可能会再次返回。使用 RememberObserver 会导致错误的生命周期回调,因此库强制使用 RetainObserver

RetainedEffect:保留后仍存在的副作用

RetainedEffect 将保留的概念扩展到副作用:

kotlin 复制代码
@Composable
public fun RetainedEffect(key1: Any?, effect: RetainedEffectScope.() -> RetainedEffectResult) {
    retain(key1) { RetainedEffectImpl(effect) }
}

private class RetainedEffectImpl(
    private val effect: RetainedEffectScope.() -> RetainedEffectResult
) : RetainObserver {
    private var onRetire: RetainedEffectResult? = null

    override fun onRetained() {
        onRetire = InternalRetainedEffectScope.effect()
    }

    override fun onRetired() {
        onRetire?.retire()
        onRetire = null
    }

    // Other callbacks are no-ops
}

实现非常简单:它将 effect 包装在一个 RetainObserver 中,该 RetainObserver 在保留时执行 effect,并在退出时进行清理。与 DisposableEffect 不同,DisposableEffect 在离开组合时运行其处置,而 RetainedEffect 仅在真正退出时处置:

kotlin 复制代码
@Composable
fun VideoPlayer(mediaUri: String) {
    val player = retain(mediaUri) { MediaPlayer(mediaUri) }

    // DisposableEffect would dispose on every hide/show
    // RetainedEffect only disposes when truly done
    RetainedEffect(player) {
        player.initialize()
        onRetire {
            player.close() // Only called when player is retired
        }
    }
}

这是一个很好的例子,说明在短暂的 UI 变化期间不应重新创建昂贵的资源。

RetainedContentHost 和 RetainScopeHolder

compose-runtime-retain 库提供了基于这些原语构建的更高级别的抽象。RetainedContentHost 管理显示/隐藏场景:

kotlin 复制代码
@Composable
public fun RetainedContentHost(active: Boolean, content: @Composable () -> Unit) {
    val retainScope = retainControlledRetainScope()
    if (active) {
        CompositionLocalProvider(LocalRetainScope provides retainScope, content)

        val composer = currentComposer
        DisposableEffect(retainScope) {
            val cancellationHandle =
                if (retainScope.keepExitedValuesRequestsFromSelf > 0) {
                    composer.scheduleFrameEndCallback {
                        retainScope.stopKeepingExitedValues()
                    }
                } else {
                    null
                }

            onDispose {
                cancellationHandle?.cancel()
                retainScope.startKeepingExitedValues()
            }
        }
    }
}

该实现揭示了一些时间控制:

变为活动状态:安排在帧结束时停止保留,确保所有内容都首先恢复其值。

变为非活动状态:在内容被移除之前立即开始保留。

这确保了值在需要时被准确保留,不会太早(这会阻止恢复),也不会太晚(这会丢失值)。

对于列表等动态内容,RetainScopeHolder 负责管理每个项目的保留:

kotlin 复制代码
public class RetainScopeHolder() {
    private val childScopes = MutableScatterMap<Any?, ControlledRetainScope>()

    public fun getOrCreateRetainScopeForChild(key: Any?): RetainScope {
        return childScopes.getOrPut(key) {
            ControlledRetainScope().apply {
                if (isParentKeepingExitedValues) startKeepingExitedValues()
            }
        }
    }

    @Composable
    public fun RetainScopeProvider(key: Any?, content: @Composable () -> Unit) {
        CompositionLocalProvider(
            LocalRetainScope provides getOrCreateRetainScopeForChild(key)
        ) {
            content()
            PresenceIndicator(key)
        }
    }

    @Composable
    private fun PresenceIndicator(key: Any?) {
        val composer = currentComposer
        DisposableEffect(key) {
            val endRetainHandle =
                if (keepExitedValuesRequestsFor(key) > 0) {
                    composer.scheduleFrameEndCallback {
                        stopKeepingExitedValues(key)
                    }
                } else {
                    null
                }
            onDispose {
                endRetainHandle?.cancel()
                startKeepingExitedValues(key)
            }
        }
    }
}

PresenceIndicator 可组合项是一种巧妙的模式。它按组合顺序放置在内容之后,以确保正确的生命周期管理。移除时,它会触发保留。添加时,它会安排在帧完成后停止保留。

内存和性能考量

该实现中进行了值得理解的权衡:

内存优先于 CPU :保留值在内存中保留的时间比 remember{} 更长。这用内存换取了重新创建时的 CPU 开销。对于开销较大的对象(例如位图、媒体播放器),这通常是值得的。

O(n) 范围操作:查找要恢复或退出的值需要迭代存储的值。对于包含数十个保留值的典型用例,这可以忽略不计。

惰性分配 :仅在需要时分配缓冲区和存储空间。未使用的 RetainScope 开销极小。

通过具体化实现类型安全:内联/具体化模式消除了运行时类型检查的需要,同时保持了类型安全。

默认为组合范围:与 ViewModel(应用范围)或 rememberSaveable(活动范围)不同,retain 是组合范围的,可以对保留边界进行细粒度的控制。

另外,需要注意的是,不要将长期存活的对象保留在其预期作用域之外,以免造成内存泄漏,正如 Compose 库中的以下注释所示。

kotlin 复制代码
/**
 * 重要提示:保留值的保存时间比其所关联的可组合项的生命周期长。
 * 如果保留对象的保存时间超过其预期的
 * 生命周期,则可能导致内存泄漏。请谨慎选择保留的数据类型。切勿保留 Android 上下文或
 * 直接或间接引用上下文(包括视图)的对象。
 */

测试保留机制

一个有趣的内部单元测试用例 表明,该库可以处理边缘情况并保证:

kotlin 复制代码
@Test
fun retain_duplicateRetainKeys() = compositionTest {
    val scope = ControlledRetainScope().apply { startKeepingExitedValues() }
    var showContent = true

    compose {
        CompositionLocalProvider(value = LocalRetainScope provides scope) {
            if (showContent) {
                // All have same key!
                retain { CountingRetainObject() }
                retain { CountingRetainObject() }
                retain { CountingRetainObject() }
            }
        }
    }

    // Hide and show content
    showContent = false
    recomposeScope.invalidate()
    advance()

    showContent = true
    recomposeScope.invalidate()
    advance()

    // All values correctly restored despite duplicate keys
}

测试验证了即使存在重复的键(在循环或生成的内容中很常见),值也能通过后进先出 (LIFO) 顺序与其保留调用正确配对。

结论

在本文中,你探索了 retain{}RetainScopeRetainObserverRetainedEffect API 的工作原理及其内部机制。了解这些内部机制有助于你更好地决定何时使用 retain{}remember{}rememberSaveable{},如何构建保留资源,以及预期的性能特征。

无论你是构建可旋转的视频播放器、保留滚动位置的导航系统,还是在配置更改后保持状态的复杂表单,retain{} 都能在 Jetpack Compose 中提供基于作用域的状态保存功能。

如果你想了解最新的技能、新闻、技术文章、面试问题和实用代码技巧,请查看 Dove Letter。如果你想深入了解面试准备,千万不要错过终极 Android 面试指南:Manifest Android Interview

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
꒰ঌ 安卓开发໒꒱4 小时前
Mysql 坏表修复
android·mysql·adb
_李小白4 小时前
【Android Gradle学习笔记】第八天:NDK的使用
android·笔记·学习
袁震5 小时前
Android-Compose 列表组件详解
android·recyclerview·compose
Sky#boy5 小时前
Kotion 常见用法注意事项(持续更新...)
kotlin
2501_916007476 小时前
提升 iOS 26 系统流畅度的实战指南,多工具组合监控
android·macos·ios·小程序·uni-app·cocoa·iphone
zh_xuan6 小时前
android 利用反射和注解绑定控件id和点击事件
android·注解·反射·控件绑定
这个杀手不太累8 小时前
Android ProcessLifecycleOwner
android·lifecycle
SRC_BLUE_1710 小时前
NSSCTF - Web | 【第五空间 2021】pklovecloud
android·前端
奥陌陌10 小时前
kotlin className.() 类名点花括号 T.() 这种是什么意思?
kotlin