前言
在第一篇文章中,我们聊了 Compose 的"去 View 化"架构。很多开发者在上手 Compose 后,最直观的感受就是:代码变少了,但"魔法"变多了。
最典型的例子就是:为什么我改了一个变量的值,UI 就会自动刷新?底层到底是谁在监控这些变化?remember 和 MutableState 又是如何配合工作的?
今天,我们就拨开重组(Recomposition)的迷雾,揭秘 Compose 核心的驱动引擎 ------ 快照系统 (Snapshot System)。
一、 为什么不能用普通的成员变量?
在 View.java 时代,我们习惯于定义一个 private var count = 0,然后在点击事件里 count++,最后手动调用 textView.text = count.toString()。
但在 Compose 中,如果你这样做:
kotlin
@Composable
fun Counter() {
var count = 0 // 普通变量
Button(onClick = { count++ }) {
Text("当前计数:$count")
}
}
你会发现,无论你怎么点,UI 纹丝不动。
核心逻辑: Compose 的 UI 是由函数执行产生的。普通变量的变化无法被 Compose 运行环境感知。要让 UI 响应变化,我们需要一种能够**"自动上报进度"**的特殊变量。
二、 MutableState:UI 的"情报员"
当我们把变量改为 mutableStateOf(0) 时,魔法开始了。
1. 代理与监听
MutableState 本质上是一个被包装过的对象。当你读取它的 value 时,Compose 会记录下:"噢,函数 A 读取了这个值";当你修改它的 value 时,Compose 会立刻通知:"所有读取过这个值的函数,你们该重新执行了!"
2. 快照系统 (Snapshot System)
这是 Compose 最硬核的底层。它类似于 Git 的版本管理:
- 隔离性:你在协程里修改状态,就像在 Feature 分支改代码,不会立刻干扰主分支。
- 原子性:只有当修改完成后(提交快照),Compose 才会统一触发 UI 刷新。
- 局部刷新 :由于它精准记录了哪个 Composable 读取了哪个 State,所以它能做到:只刷新受影响的那一行文字,而跳过整个页面的其他部分。
三、 remember:函数的"记忆力"
如果你只用了 MutableState,可能还是会遇到问题:
kotlin
@Composable
fun Counter() {
val countState = mutableStateOf(0) // 每次重组都会重新初始化为 0!
// ...
}
因为 Composable 本质是函数。每次 UI 刷新,函数都会从头执行一遍。
remember 的作用:
它像是一个**"时光胶囊"**。
- 第一次运行 :执行
mutableStateOf(0),并把结果存入 Compose 维护的"槽位表 (Slot Table)"中。 - 第二次运行(重组) :
remember会直接从槽位表中把上次存的结果拿出来,而不是重新跑一遍初始化。
Tips :remember 的生命周期是绑定在当前 Composable 在 UI 树中的位置上的。如果 Composable 从树上被移除了,这块内存也会被回收。
四、 局部刷新的底层真相:Slot Table (槽位表)
为什么 Compose 嵌套了十层,改一个小地方性能依然很高?
Compose 内部维护了一张巨大的表(Slot Table),记录了 UI 的结构和绑定的状态。
- 当状态改变,Compose 进入 Recomposition 阶段。
- 它会比对新旧状态。
- 如果一个 Composable 函数的输入参数没变,且它读取的全局状态也没变,Compose 就会直接 跳过(Skip) 这个函数。
这与传统 View 体系中 requestLayout() 动不动就全量递归测量有着本质的区别。
五、 避坑指南:给开发的三个忠告
- 不要在 Composable 函数体里写副作用 : 比如
count++或网络请求。因为重组随时可能发生,你的逻辑会被执行无数次。请使用LaunchedEffect。 - 善用
derivedStateOf: 如果你有一个状态是根据另一个状态算出来的(比如列表是否滚动到底部),请用derivedStateOf进行包装,防止无效的频繁重组。 - 状态提升 (State Hoisting): 尽可能让你的 Composable 变成"无状态"的。把状态交给 ViewModel 管理,这不仅是为了测试,更是为了让 UI 层逻辑更纯粹。
结语
理解了快照系统 和槽位表 ,你就抓住了 Compose 的灵魂。在下一篇文章中,我们将深入探讨 Compose 的布局协议 ,看看它如何实现 O(n) 的单次测量,彻底解决嵌套性能难题。
如果你觉得有帮助,欢迎点赞关注,我们在代码上演进,在原理上深耕。