理解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

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

保护原创,请勿转载!

相关推荐
曹绍华5 小时前
kotlin扩展函数是如何实现的
android·开发语言·kotlin
LSL666_10 小时前
5 Repository 层接口
android·运维·elasticsearch·jenkins·repository
alexhilton13 小时前
在Jetpack Compose中创建CRT屏幕效果
android·kotlin·android jetpack
2501_9400940215 小时前
emu系列模拟器最新汉化版 安卓版 怀旧游戏模拟器全集附可运行游戏ROM
android·游戏·安卓·模拟器
下位子15 小时前
『OpenGL学习滤镜相机』- Day9: CameraX 基础集成
android·opengl
参宿四南河三17 小时前
Android Compose SideEffect(副作用)实例加倍详解
android·app
火柴就是我18 小时前
mmkv的 mmap 的理解
android
没有了遇见18 小时前
Android之直播宽高比和相机宽高比不支持后动态获取所支持的宽高比
android
shenshizhong19 小时前
揭开 kotlin 中协程的神秘面纱
android·kotlin
vivo高启强19 小时前
如何简单 hack agp 执行过程中的某个类
android