本文译自「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{}
、RetainScope
、RetainObserver
和 RetainedEffect
的内部机制,探索它们的底层工作原理,以及使 Retention 既内存安全又性能卓越的优化方法。
如果你尚未阅读以下之前的文章,理解以下部分将对你有所帮助:
- 预览 retain{} API:Jetpack Compose 中持久化状态的新方法
- 预览 RetainedEffect:桥接 Composition 和 Retention 生命周期的新 Side Effect
导入 Compose 运行时 retain 库
你可以使用以下依赖项导入 Compose 运行时 retain 库:
kotlin
implementation("androidx.compose.runtime:runtime-retain:1.10.0-alpha05")
请注意,此库目前处于实验阶段,未来将不断完善和稳定发布。
理解核心问题:为什么 remember{} 还不够
从本质上讲,remember{}
可以在单个组合生命周期内跨重组保留状态。但是,当你的 Activity 因配置更改而重新创建时会发生什么?或者当导航目标从返回堆栈中移除时?状态会丢失,你的可组合项会重新开始。
传统的 Android 解决方案涉及 ViewModel
和 SavedStateHandle
,但它们本身也具有复杂性:序列化要求、手动状态恢复以及在可组合项层次结构之外管理状态的认知开销。如果我们可以拥有一个像 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()
}
作用域有两种运行模式:
- 普通模式 (
isKeepingExitedValues = false
):值离开组合时会被丢弃,就像remember{}
一样。 - 保留模式 (
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 │
└──────────────────────────┘
此流程揭示了几个关键的见解:
- 保留是有条件的 :只有当
isKeepingExitedValues
为true
时,值才会被保留。 - 键很重要:即使在保留期间,更改的键也会导致立即退出。
- 恢复是自动的:当内容使用相同的键重新输入时,保留的值将被恢复。
- 退出是最终的:保留停止时未恢复的值将被退出。
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
回调(onRemembered
、onForgotten
、onAbandoned
)与保留语义不一致。暂时脱离组合的保留值不会被"遗忘",而是处于不确定状态,可能会再次返回。使用 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{}
、RetainScope
、RetainObserver
和 RetainedEffect
API 的工作原理及其内部机制。了解这些内部机制有助于你更好地决定何时使用 retain{}
、remember{}
和 rememberSaveable{}
,如何构建保留资源,以及预期的性能特征。
无论你是构建可旋转的视频播放器、保留滚动位置的导航系统,还是在配置更改后保持状态的复杂表单,retain{}
都能在 Jetpack Compose 中提供基于作用域的状态保存功能。
如果你想了解最新的技能、新闻、技术文章、面试问题和实用代码技巧,请查看 Dove Letter。如果你想深入了解面试准备,千万不要错过终极 Android 面试指南:Manifest Android Interview。
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!