1. remember 基本机制:重组上下文、缓存作用域与值的关系
1.1 核心原理
remember 是 Jetpack Compose 中最基础的状态持有函数。它的本质是一个惰性计算+缓存委托:在第一次组合时执行传入的 lambda 并保存结果,在后续重组中,只要 key 参数(如果有)未变,直接返回缓存值而不重新执行 lambda。这一机制将 Composable 函数的任意次调用与 单一状态实例 解耦,是避免重复昂贵计算的关键。
记住核心等式:
remember(key1, key2, ...) { 计算 } ⇒ 当所有 key 值与上次重组时相等 ⇒ 返回旧缓存;否则执行 lambda 并缓存新值。
1.2 重组上下文与缓存作用域
重组上下文指每次 Composable 函数被重新执行时,运行时为它分配的快照环境。而 remember 的缓存依附于调用它在组合树中的位置(即 composePosition)。同一个 Composable 函数在不同的调用路径中即使参数相同,也会产生独立的 remember 实例。
| 属性 | 说明 |
|---|---|
| 缓存作用域 | 当前 Composable 函数在组合树中的唯一位置(由组合键标识) |
| 存活期 | 从首次进入组合树到被移出组合树(即 dispositon) |
| 作用域内共享 | 同一位置多次重组共享同一个缓存实例 |
| 作用域隔离 | 不同位置(如列表项)彼此不共享缓存 |
1.3 值与重组的关系
remember 本身不触发重组。它的返回值是普通 Kotlin 对象(不可观察),只有当被 mutableStateOf 或 derivedStateOf 包裹后,值的改变才会驱动重组。因此 remember 常用作以下场景的容器:
- 存储昂贵计算结果(如复杂对象、资源加载)
- 保存不需要驱动 UI 重组的静态数据
- 为
mutableStateOf提供初始值或计算中间结果
下图展示了 remember 的完整工作流程:
1.4 实现流程(步骤化)
- 首次组合:Composable 函数执行到
remember调用时,运行时检查本地缓存是否已初始化(找不到)。 - 计算 lambda:执行传入的
{ 计算 }并得到返回值。 - 存储:将返回值保存在组合树当前节点的槽位(slot)中。
- 返回:将计算结果直接返回给调用者。
- 重组触发:当上游状态改变导致当前 Composable 被重新调用,运行时重入相同位置。
- key 比较:若提供了 key 参数,逐个与上次保存的 key 值进行相等性比较(
equals)。 - 命中/未命中:所有 key 相等 ⇒ 直接返回槽位中的缓存值;否则执行 lambda 并用新值覆盖槽位。
提示:
remember 的 key 默认使用 Unit(即无 key),此时仅第一次计算,后续重组永远命中缓存。显式提供 key 可以控制缓存失效时机,但需注意 key 必须是稳定可比较的类型(如 Int/Boolean/String),避免使用不可比较的普通对象。
1.5 代码示例:简单计数器
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
// remember 在这里只存储 mutableStateOf 创建的委托对象,
// 对象本身不变,但内部值变化会触发重组。
// 如果去掉 remember,每次重组都会创建新的 mutableStateOf,
// 会导致状态丢失。
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("增加")
}
}
}
上例中 remember 确保 mutableStateOf(0) 在整个组合生命周期内只创建一次,否则每次重组都会重置为 0。这是 remember 最经典的用法。
1.6 调试技巧
- 日志验证:在 lambda 内加
Log.d打印日志,观察是否多次执行。 - Compose Layout Inspector:通过 Android Studio 的 Layout Inspector 查看重组次数,确认
remember是否命中缓存。 - key 调试:临时将 key 改为
System.currentTimeMillis(),观察 UI 是否异常频繁刷新(证明 key 影响缓存)。
常见错误:在非 Composable 作用域使用
remember 只能在 @Composable 函数或 lambda 中调用。在普通 Kotlin 函数或 launch 内部调用会抛出 IllegalStateException。解决方案:通过参数将状态传入,或使用 remember 包装的可观察对象。
1.7 常见错误及修正
| 错误场景 | 错误代码 | 修正 |
|---|---|---|
| 忘记 key 导致状态不随参数更新 | |
|
| 在列表项中使用全局 remember(导致所有项共享状态) | Item 内使用 remember { mutableStateOf(0) } |
每个 Item 自身 remember 已在各自组合位置隔离,无需修改。关键在于不要在 Item 外部的单例中存放状态。 |
| lambda 内捕获了不该捕获的可变对象 | someVar 在重组间变化但不作为 key |
将 someVar 加入 remember(someVar) 的 key 列表中 |
1.8 性能优化建议
- 减少 lambda 开销:如果计算本身轻量,甚至不需要
remember,直接求值即可。 - 合理设置 key:不要使用过大的集合作为 key(如整个 List),改用
id或稳定标识。 - 避免缓存过大对象:
remember会持有引用直到组合被移除,注意内存泄漏。 - 与
derivedStateOf配合:当需要从多个状态派生值时,将派生逻辑放入derivedStateOf并用remember包裹,避免每次重组重新计算。
性能底线:
remember 本身是 O(1) 的键值查找,不会成为性能瓶颈。瓶颈通常发生在 lambda 内的计算或捕获的闭包重新创建大量对象。始终优先优化 lambda 内部逻辑。