解析 Compose 的核心概念 remember

嗨,各位开发者!今天咱们来聊聊 Compose 里最核心的概念之一------remember 函数。

如果你也曾疑惑:

为什么重组时 UI 状态会丢失?

Compose 又是如何把数据保存在内存中的?

这篇文章绝对能让你豁然开朗。

我会从 Compose 底层架构出发,把 remember 彻底讲透。

一切问题的起源

在深入 remember 之前,我们先搞懂它要解决的核心问题。以一个最简单的计数器应用为例:

kotlin 复制代码
@Composable
fun Counter() {
    var count = 0  // 千万别这么写!

    Button(onClick = { count++ }) {
        Text("已点击 $count 次")
    }
}

运行代码、点击按钮后你会发现......界面毫无变化,计数器始终停在 0。

原因很简单:Compose 会对组合函数进行多次重组(重新执行)

每次 UI 需要更新时,Counter() 函数都会被重新调用,count 也会被重新赋值为 0。就像你数数时,每数一秒就忘记之前的数字,这显然无法满足需求。

remember:状态守护者

这时候 remember 就派上用场了:

kotlin 复制代码
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }  // 这样才对!

    Button(onClick = { count++ }) {
        Text("已点击 $count 次")
    }
}

现在功能正常了!但底层到底发生了什么?

来,我们深挖架构原理。

核心架构

1. 组合树(Composition Tree)

编写组合函数时,Compose 并非只渲染一次就丢弃,而是会构建一棵组合树------可以理解为承载整个 UI 结构的树形数据结构。

css 复制代码
Composition(Root 节点)
├── Counter(节点)
│   ├── Button(节点)
│   │   └── Text(节点)

每个组合函数都会成为树上的一个节点。

更关键的是,Compose 不只是存储 UI 元素,还会为每个节点预留存储数据的插槽

2. 插槽表(Slot Table)

remember 的底层魔法,全靠插槽表实现。

你可以把插槽表简单理解为一个大容量数组(实际实现更复杂),Compose 会在其中存储:

  • UI 结构信息
  • 组合函数的入参
  • 被记忆的状态值(也就是我们的核心数据)
  • 每个组合函数的元数据
css 复制代码
Slot Table(简化版)
┌─────────────────────────────────────┐
│ Slot 0:Counter 组合元数据           │
│ Slot 1:记忆值(count = 0)          │ ← 我们的状态就存在这里!
│ Slot 2:Button 组合函数元数据        │
│ Slot 3:Text 组合函数元数据          │
└────────────────────────────────────┘

3. 组合器(Composer)

组合器就像乐队的指挥,负责管控所有流程:

  • 当前在插槽表中的位置(类似游标)
  • 正在执行的组合函数
  • 每个位置需要记忆的数值

组合函数执行时,组合器会遍历插槽表,按需读写数据。

remember 底层执行流程

现在,演员已经各就各位,让我们一步步追踪 remember 的逻辑,看看它是怎么工作的:

1. 首次组合(初始渲染)

kotlin 复制代码
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }  // 首次执行
    // ...
}
  1. 组合器进入 Counter:从插槽表中对应 Counter 的位置开始读取
  2. 遇到 remember:组合器识别到记忆函数
  3. 检查插槽表:当前位置是否已有存储值?
  4. 无值(首次执行):执行 lambda 代码 { mutableStateOf(0) }
  5. 存入插槽表:将结果(值为 0MutableState 对象)写入插槽
  6. 返回值:remember 返回存储的对象

2. 重组(用户点击按钮)

kotlin 复制代码
Button(onClick = { count++ }) { ... }

用户点击按钮,count0 变为 1,触发重组:

  1. 触发重组:Compose 检测到状态变更
  2. 组合器重新进入 Counter:再次执行 Counter 函数
  3. 再次遇到 remember:执行同一行代码
  4. 检查插槽表:当前位置已有值
  5. 跳过计算:不再执行 lambda 表达式
  6. 返回缓存值:直接返回已存在的 MutableState 对象(值为 1)

核心结论:remember 里的 lambda 仅在首次组合时执行一次,后续重组直接返回缓存值。

remember 到底长什么样

我们来看 Compose 源码中 remember 的简化实现:

kotlin 复制代码
@Composable
inline fun <T> remember(calculation: () -> T): T {
    return currentComposer.cache(false, calculation)
}

表面上看,极度简单,但是核心魔法全在 cache 里:

kotlin 复制代码
// 简化版缓存逻辑
fun <T> cache(invalid: Boolean, calculation: () -> T): T {
    val value = nextSlot()  // 从插槽表获取下一个插槽

    if (value === Composer.Empty || invalid) {
        // 插槽为空或已失效 → 计算新值 → 调用传入的 lambda 表达式 calculation
        val newValue = calculation()
        updateValue(newValue)  // 存入插槽表
        return newValue
    } else {
        // 插槽已有值 → 直接返回
        return value as T
    }
}

Compose 如何定位数据

你可能会疑惑:"Compose 怎么知道哪个 remember 对应哪个插槽?"

答案是:位置记忆化(调用位点记忆)

Compose 不依赖变量名或显式 Key,而是通过三个维度定位:

  1. 调用顺序
  2. 代码中的调用位置
  3. 父组合函数
kotlin 复制代码
@Composable
fun Example() {
    val a = remember { 1 }  // 插槽位置:Example.0
    val b = remember { 2 }  // 插槽位置:Example.1

    if (condition) {
        val c = remember { 3 }  // 插槽位置:Example.2
    }
}

每个 remember 都会根据调用顺序获得唯一插槽位置。

⚠️ 重要警告:谨慎在循环、条件语句中随意使用 remember

kotlin 复制代码
// 谨慎!最好不要别这么做
@Composable
fun BadExample(items: List<String>) {
    for (item in items) {
        val state = remember { mutableStateOf(item) }  // 插槽位置会乱!
    }
}

如果 items 里面的发生变更(例如增加数据,删除数据),remember 的调用位置会改变,Compose 会无法匹配插槽与数据。

解决方案:使用 key 或带 keyremember

kotlin 复制代码
// 正确写法
@Composable
fun GoodExample(items: List<String>) {
    items.forEach { item ->
        key(item) {  // 创建新的组合作用域
            val state = remember { mutableStateOf(item) }
        }
    }
}

黄金搭档:remember 与 mutableStateOf

很多初学者会混淆这两个 API,我在这里理清一下他俩的区别:

mutableStateOf:可观察数值

kotlin 复制代码
val count = mutableStateOf(0)

创建一个 Compose 可观察的 MutableState<Int> 对象。值变更时,Compose 会自动重组读取该值的组合函数。

简化版底层实现:

kotlin 复制代码
class MutableState<T>(private var value: T) {
    fun get(): T {
        // 注册当前组合函数为订阅者
        currentComposer.recordRead(this)
        return value
    }

    fun set(newValue: T) {
        value = newValue
        // 通知所有订阅者重组
        notifySubscribers()
    }
}

remember:状态持久化

kotlin 复制代码
val count = remember { mutableStateOf(0) }

remember 保证 MutableState 对象在重组中存活

如果没有 remember,每次重组都会创建新的 MutableState,之前的状态会彻底丢失。

通俗比喻:

  • mutableStateOf 是一个会主动通知变化的智能闹钟
  • remember 是存放闹钟的架子,保证它不会被重组丢弃

或者这么说,大家存过钱吧!

  • remember:在银行开了一个账户------账户的"身份"在重组之后还在,不会每次进银行都换一个新账户。
  • mutableStateOf:账户里的余额可以存、可以取,而且一变动,银行会通知相关业务(触发重组)。

如果不用 remember:每次重组都相当于"又开了一个新账户",之前的余额(状态)全没了,界面看起来就像永远在"重置"。

高级用法与变体

1. 带参 remember

前面那个循环里面用 remember 的另一个解法,就是使用带参数的 remember

kotlin 复制代码
@Composable
fun UserProfile(userId: String) {
    val userData = remember(userId) {
        fetchUserData(userId)  // 仅当 userId 变化时重新计算
    }
}

remember 传入参数后,仅当参数变化时才会重新计算。

底层逻辑:Compose 会同时存储计算值与入参,重组时对比新旧参数,不一致则重新计算。

2. rememberSaveable:扛住配置变更

kotlin 复制代码
var count by rememberSaveable { mutableStateOf(0) }

remember 仅在组合生命周期内存储状态,一旦页面旋转、进程被杀,组合销毁,所有记忆值都会丢失。

rememberSaveable 会将值存入 Bundle(类似 savedInstanceState),保证配置变更、进程恢复后状态不丢失。

当然,一提到配置变更,进程恢复,那么说明 rememberSaveable 里面的用到的数据必须保证能够序列化,

简化版底层逻辑:

kotlin 复制代码
@Composable
fun rememberSaveable<T>(calculation: () -> T): T {
    // 1. 尝试从保存的状态中恢复
    val restored = restoreFromBundle(currentComposer.currentKey)
    if (restored != null) {
        return restored as T
    }
    // 2. 未恢复则创建并记忆
    val value = remember(calculation)
    // 3. 注册配置变更时的保存逻辑
    DisposableEffect(Unit) {
        registerForSaving(currentComposer.currentKey, value)
        onDispose { unregister() }
    }
    return value
}

3. 缓存耗时计算

kotlin 复制代码
@Composable
fun ExpensiveComputation(data: List<Int>) {
    val result = remember(data) {
        // 仅当 data 变化时执行耗时计算
        data.map { heavyCalculation(it) }
    }
}

适用场景:

  • 计算逻辑耗 CPU
  • 不想每次重组都重新计算
  • 结果依赖特定入参

常见采坑

1. 忘记使用 remember

kotlin 复制代码
// 错误
@Composable
fun Counter() {
    var count by mutableStateOf(0)  // 重组必丢状态!
    Button(onClick = { count++ }) { Text("$count") }
}

// 正确
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) { Text("$count") }
}

大家这里其实可以记住一个要诀,但凡是在 Compose 中出现类似 val a = xxx 的代码,即没有使用 remember,那你一定要想仔细看这个地方,我是不是用错了?

2. 记忆不需要持久化的值

kotlin 复制代码
// 错误:时间会卡在首次组合的时刻
@Composable
fun Clock() {
    val time = remember { System.currentTimeMillis() }
    Text("Time: $time")
}

// 正确:需要主动更新
@Composable
fun Clock() {
    var time by remember { mutableStateOf(System.currentTimeMillis()) }

    LaunchedEffect(Unit) {
        while (true) {
            delay(1000)
            time = System.currentTimeMillis()
        }
    }

    Text("Time: $time")
}

3. 内存管理

每个 remember 都会在插槽表中占用空间,少量使用无影响,但大量列表需注意:

kotlin 复制代码
// 不推荐:大量数据占用大量插槽
@Composable
fun HugeList(items: List<String>) {
    items.forEach { item ->
        val state = remember { mutableStateOf(item) }
    }
}

这个方案还有另一个问题------每次 items 的变更,都会触发全量的 remember 重新初始化。

上面有个类似的例子,提到过这个问题

推荐方案:结合 LazyColumn + key

kotlin 复制代码
// 推荐
@Composable
fun HugeList(items: List<String>) {
    LazyColumn {
        items(items, key = { it }) { item ->
            // 状态与 item key 绑定
            val state = remember { mutableStateOf(item) }
        }
    }
}

推荐场景

推荐:

  • 存储需要在重组中存活的状态
  • 缓存耗时计算结果
  • 仅需创建一次的对象

不推荐:

  • 值直接来源于入参
  • 需要扛住配置变更(改用 rememberSaveable
  • 需要跨组合共享状态(用 ViewModel / 状态提升)

通俗总结

把 Compose 想象成一场剧场演出:

  1. 剧本 = 组合函数
  2. 舞台 = 插槽表
  3. 导演 = 组合器
  4. 道具 = 被记忆的状态
  5. 重演 = 重组

使用 remember,就是把道具放在舞台固定位置,导演记住位置,每次重演都用同一个道具,只有主动更换或 Key 变化时才会替换。

实战案例:计时器

我们把所有知识点整合为一个完整计时器:

kotlin 复制代码
@Composable
fun Timer() {
    // 记忆倒计时秒数
    var elapsedSeconds by remember { mutableStateOf(0) }
    // 记忆运行状态
    var isRunning by remember { mutableStateOf(false) }

    // 启停逻辑
    LaunchedEffect(isRunning) {
        if (isRunning) {
            while (isRunning) {
                delay(1000)
                elapsedSeconds++
            }
        }
    }

    Column {
        Text("时间:${elapsedSeconds}秒")

        Button(onClick = { isRunning = !isRunning }) {
            Text(if (isRunning) "暂停" else "开始")
        }

        Button(onClick = { elapsedSeconds = 0 }) {
            Text("重置")
        }
    }
}

执行逻辑:

  1. elapsedSecondsisRunning 被记忆,重组不丢失
  2. 点击「开始」,isRunning 变化触发重组
  3. LaunchedEffect 感知变化,启动计时
  4. 每秒更新 elapsedSeconds,触发重组
  5. 计时器持续运行,不受重组影响

写在最后

remember 只有一个单词,却封装了 Compose 精巧的内存管理体系。

理解这四点,你就掌握了 Compose 状态管理的核心:

  • 组合树:UI 的结构载体
  • 插槽表:状态的存储位置
  • 组合器:状态的调度核心
  • 位置记忆化:状态的定位规则

掌握这些原理,你能:

  • 写出更高效的 Compose UI
  • 快速排查状态相关 Bug
  • 合理选择 rememberrememberSaveableViewModel

记住:Compose 很智能,但需要你明确告诉它------这个值需要跨重组保存

下面给出一个实战速查表:

场景 解决方案
组合函数内需要状态 var state by remember { mutableStateOf(value) }
扛住配置变更/进程恢复 var state by rememberSaveable { mutableStateOf(value) }
缓存耗时计算 val result = remember(key) { 耗时操作() }
跨组合共享状态 状态提升 或 使用 ViewModel
列表项带状态 使用 key()items(items, key = {})
需要重置记忆值 传入变化的 Key:remember(resetKey) { ... }

现在,去写出状态稳定的 Compose 界面吧!🚀

相关推荐
y = xⁿ几秒前
MySQL八股知识合集
android·mysql·adb
andr_gale35 分钟前
04_rc文件语法规则
android·framework·aosp
祖国的好青年2 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴2 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭2 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首2 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil3 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙4 小时前
echarts,3d堆叠图
android·3d·echarts
李白的天不白4 小时前
如何项目发布到github上
android·vue.js