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 的完整工作流程:

remember 工作流程图 重组开始 检查 key 是否变化 key未变 key变化 返回缓存值 执行 lambda 存储并返回 重组继续 关键点 • 缓存依附于组合位置 • key 控制重新计算的触发

1.4 实现流程(步骤化)

  1. 首次组合:Composable 函数执行到 remember 调用时,运行时检查本地缓存是否已初始化(找不到)。
  2. 计算 lambda:执行传入的 { 计算 } 并得到返回值。
  3. 存储:将返回值保存在组合树当前节点的槽位(slot)中。
  4. 返回:将计算结果直接返回给调用者。
  5. 重组触发:当上游状态改变导致当前 Composable 被重新调用,运行时重入相同位置。
  6. key 比较:若提供了 key 参数,逐个与上次保存的 key 值进行相等性比较(equals)。
  7. 命中/未命中:所有 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 { calcExpensive(price) }
remember(price) { calcExpensive(price) }
在列表项中使用全局 remember(导致所有项共享状态)
repeat(5) { Item() }

Item 内使用 remember { mutableStateOf(0) }
每个 Item 自身 remember 已在各自组合位置隔离,无需修改。关键在于不要在 Item 外部的单例中存放状态。
lambda 内捕获了不该捕获的可变对象
remember { expensiveFun(someVar) }

someVar 在重组间变化但不作为 key
将 someVar 加入 remember(someVar) 的 key 列表中

1.8 性能优化建议

  • 减少 lambda 开销:如果计算本身轻量,甚至不需要 remember,直接求值即可。
  • 合理设置 key:不要使用过大的集合作为 key(如整个 List),改用 id 或稳定标识。
  • 避免缓存过大对象:remember 会持有引用直到组合被移除,注意内存泄漏。
  • 与 derivedStateOf 配合:当需要从多个状态派生值时,将派生逻辑放入 derivedStateOf 并用 remember 包裹,避免每次重组重新计算。
性能底线: remember 本身是 O(1) 的键值查找,不会成为性能瓶颈。瓶颈通常发生在 lambda 内的计算或捕获的闭包重新创建大量对象。始终优先优化 lambda 内部逻辑。