什么是重组(Recomposition):基础概念与触发条件
重组,说白了就是 Compose 在状态变化后重新执行你的 Composable 函数,生成新的 UI 片段。你不需要手动写 setText()、notifyDataSetChanged() 那一套,把状态交给 Compose,它自动盯着变化,变了就重跑。
我刚从 View 系统转到 Compose 时,总觉得这玩意儿是个黑盒。后来跟踪了几次重组日志才明白——它其实是一个有范围的“局部刷新”。不是整个界面推倒重来,只更新变了的那部分。
重组到底干了什么?
每个 Composable 函数本质是一个代码块。Compose 会记住它的输入参数、读取到的状态,以及当前的执行位置。当某个状态变量变化时,Compose 找到“读过这个状态”的 Composable 作用域,重新调用它们。
- 只重跑读取了变化状态的代码块,不是整个 UI 树
- 不是 View 的重新布局或重绘,只是函数的重新执行
- 执行结果生成新的 LayoutNode 树,然后 Compose 对比差异,高效更新
触发重组的三个核心条件
不是所有代码变化都会触发重组。只有 Compose 能够“观察”到的状态变化才行。我整理了一个常见触发条件表:
| 触发来源 | 具体场景 | 是否可跳过 |
|---|---|---|
| MutableState 变化 | mutableStateOf、remember { mutableStateOf } 的 .value 被重新赋值 | 可跳过,前提是读取该状态的 Composable 参数没变 |
| StateFlow/LiveData 收集 | collectAsState()、observeAsState() 在协程/生命周期中发射新值 | 可跳过,框架会自动处理 |
| 输入事件回调 | 用户点击、滑动、键盘事件导致状态变更 | 不会跳过,必须触发重组 |
| 系统配置变化 | 屏幕旋转、深色模式切换、字体缩放 | 会触发整个 Activity 级别的重组 |
| Composable 参数变化 | 父 Composable 传入的参数值发生改变 | 可跳过,使用 key 或 derivedStateOf 优化 |
重组作用域:不是所有 Composable 都重跑
Compose 以“重组作用域”(Recompose Scope)为单位来执行。每个 Composable 函数内的花括号都可能被拆成多个作用域,取决于 Compose 编译插件的优化。
- 一个 Composable 函数可以包含 多个作用域
- 只有读取了变化状态的那个作用域会被重新执行
- 兄弟作用域、父作用域如果没读这个状态,就不会被重跑
举个例子:
@Composable
fun MyScreen() {
var text by remember { mutableStateOf("Hello") }
// 作用域 A:读取 text
Text(text = text)
// 作用域 B:没读 text
Button(onClick = { text = "World" }) {
Text("Change")
}
}
这里 Button 所在的 Composable 不会因为 text 变化而重跑,只有 Text 那部分会重跑。这就是 Compose 高效的秘诀。
重组的幕后推手:Snapshot 系统
Compose 底层靠 Snapshot(快照) 来记录状态读写。每次状态变化都会产生一个新版本号,Compose 收集所有“读过该状态”的记录,然后批量调度重组。
- 读操作:把当前 Composable 作用域注册到状态的观察者列表
- 写操作:标记所有注册过该状态的 Compose 作用域为“需要重组”
- 调度:在下一帧动画或布局前,以最小粒度执行重组
我把这个过程画成了流程图,方便你理解:
重组 vs 布局 vs 绘制:别再搞混了
很多新手把重组和布局混为一谈。我直接列个对比表:
| 阶段 | 做什么 | 是否频繁 | 性能影响 |
|---|---|---|---|
| 重组 | 执行 Composable 函数,生成 UI 描述 | 可能一帧多次 | 轻量,但函数体内有耗时操作会卡 |
| 布局 | 计算宽高和位置 | 一帧一次 | 中等,复杂布局会耗时 |
| 绘制 | 将布局结果渲染到屏幕 | 一帧一次 | 重,涉及 GPU 操作 |
你想想看:如果重组阶段做了大量计算,比如读取数据库、解析 JSON,那即使后面布局和绘制没变,整帧也会被拖慢。这就是重组抖动最常见的根因。
小结:记住这三句话
- 重组是响应式的——你只管声明 UI 和状态,Compose 自己知道什么时候更新。
- 重组是有范围的——不是全屏重绘,只有读了变化状态的那段代码重跑。
- 重组不是免费午餐——如果函数体里有昂贵操作,该优化还是要优化。
这一章把最基础的概念讲清楚了。后面我们会深入那些让你头疼的“后台状态变更导致重组抖动”问题——比如协程里改了 state、在非 Compose 线程改状态、还有那些莫名其妙的多余重组。到时候咱们再具体分析。