本文译自「Understanding retain{} internals: A Scope-based State Preservation in Jetpack Compose」,原文链接https://proandroiddev.com/understanding-retain-internals-a-new-way-to-preserve-state-in-jetpack-compose-54471a32fd05 ,由Jaewoong Eum发布于2025年10月15日。
Jetpack Compose 是一种现代 Android UI 开发方式,拥有声明式方法和强大的状态管理原语。虽然 remember{} 能够很好地在重组过程中保存状态,但它有一个根本性的局限性:它无法在配置更改或导航转换后继续生效。引入 retain{},这是一种在瞬时销毁场景下保存状态的新方法,同时保持可组合性。
在本文中,你将深入了解 retain{}、RetainScope、RetainObserver 和 RetainedEffect 的内部机制,探索它们的底层工作原理,以及使 Retention 既内存安全又性能卓越的优化方法。
如果你尚未阅读以下之前的文章,理解以下部分将对你有所帮助:
导入 Compose 运行时 retain 库
你可以使用以下依赖项导入 Compose 运行时 retain 库:
1
implementation ( "androidx.compose.runtime:runtime-retain:1.10.0-alpha05" )
请注意,此库目前处于实验阶段,未来将不断完善和稳定发布。
理解核心问题:为什么 remember{} 还不够
从本质上讲,remember{} 可以在单个组合生命周期内跨重组保留状态。但是,当你的 Activity 因配置更改而重新创建时会发生什么?或者当导航目标从返回堆栈中移除时?状态会丢失,你的可组合项会重新开始。
传统的 Android 解决方案涉及 ViewModel 和 SavedStateHandle,但它们本身也具有复杂性:序列化要求、手动状态恢复以及在可组合项层次结构之外管理状态的认知开销。如果我们可以拥有一个像 remember{} 一样工作,但又能在这些瞬时销毁后继续存在的东西,那会怎样?
1
2
3
4
5
6
7
8
9
10
11
// 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 类似,但存在以下区别:
1
2
3
4
5
@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 修饰符无需显式类参数即可实现类型安全的保留。但真正的魔力在于其实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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 的生命周期挂钩:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 是保留系统的核心概念。它管理何时保留值、如何存储它们以及何时恢复或退出它们。让我们来看看它的细节:
1
2
3
4
5
6
7
8
9
10
11
12
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):值退出时会被保留,以便将来恢复。
那么,保留生命周期是如何运作的呢?通过原始源代码中嵌入的图表可以最好地理解保留生命周期:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──────────────────────┐
│ │
│ 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 才是主要的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 调用可以具有相同的键:
1
2
3
4
5
6
@Composable
fun Example () {
// Both have the same positional key!
val value1 = retain { "First" }
val value2 = retain { "Second" }
}
该映射按 LIFO(后进先出)顺序存储每个键的值。恢复时,removeLast() 确保值按其存储的相反顺序恢复,从而保持 retain 调用与其值之间的正确配对。
此外,ControlledRetainScope 支持嵌套在父级 RetainStateProvider 下:
1
2
3
4
5
6
7
8
9
10
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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{} 无法实现的资源管理模式。设想一个媒体播放器,当屏幕未显示时应该暂停,但如果可能再次显示,则不应释放资源:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 中找到一个有趣的安全检查:
1
2
3
4
5
6
7
8
9
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 将保留的概念扩展到副作用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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 仅在真正退出时处置:
1
2
3
4
5
6
7
8
9
10
11
12
13
@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 管理显示/隐藏场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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 负责管理每个项目的保留:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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(Activity范围)不同,retain 是组合范围的,可以对保留边界进行细粒度的控制。
另外,需要注意的是,不要将长期存活的对象保留在其预期作用域之外,以免造成内存泄漏,正如 Compose 库中的以下注释所示。
1
2
3
4
5
6
/**
* 重要提示:保留值的保存时间比其所关联的可组合项的生命周期长。
* 如果保留对象的保存时间超过其预期的
* 生命周期,则可能导致内存泄漏。请谨慎选择保留的数据类型。切勿保留 Android 上下文或
* 直接或间接引用上下文(包括视图)的对象。
*/
测试保留机制
一个有趣的内部单元测试用例 表明,该库可以处理边缘情况并保证:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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 。