
嗨,各位开发者!今天咱们来聊聊 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) } // 首次执行
// ...
}
- 组合器进入
Counter:从插槽表中对应Counter的位置开始读取 - 遇到
remember:组合器识别到记忆函数 - 检查插槽表:当前位置是否已有存储值?
- 无值(首次执行):执行
lambda代码{ mutableStateOf(0) } - 存入插槽表:将结果(值为
0的MutableState对象)写入插槽 - 返回值:
remember返回存储的对象
2. 重组(用户点击按钮)
kotlin
Button(onClick = { count++ }) { ... }
用户点击按钮,count 从 0 变为 1,触发重组:
- 触发重组:Compose 检测到状态变更
- 组合器重新进入
Counter:再次执行Counter函数 - 再次遇到
remember:执行同一行代码 - 检查插槽表:当前位置已有值
- 跳过计算:不再执行
lambda表达式 - 返回缓存值:直接返回已存在的
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,而是通过三个维度定位:
- 调用顺序
- 代码中的调用位置
- 父组合函数
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 或带 key 的 remember
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 想象成一场剧场演出:
- 剧本 = 组合函数
- 舞台 = 插槽表
- 导演 = 组合器
- 道具 = 被记忆的状态
- 重演 = 重组
使用 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("重置")
}
}
}
执行逻辑:
elapsedSeconds、isRunning被记忆,重组不丢失- 点击「开始」,
isRunning变化触发重组 LaunchedEffect感知变化,启动计时- 每秒更新
elapsedSeconds,触发重组 - 计时器持续运行,不受重组影响
写在最后
remember 只有一个单词,却封装了 Compose 精巧的内存管理体系。
理解这四点,你就掌握了 Compose 状态管理的核心:
- 组合树:UI 的结构载体
- 插槽表:状态的存储位置
- 组合器:状态的调度核心
- 位置记忆化:状态的定位规则
掌握这些原理,你能:
- 写出更高效的 Compose UI
- 快速排查状态相关 Bug
- 合理选择
remember、rememberSaveable、ViewModel
记住: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 界面吧!🚀