重组基本原理与性能影响
2.1 Composable 函数调用与重组
Compose 框架在运行时将 Composable 函数视为可多次调用的“状态-观察-重构”单元。每次状态变化触发新一帧渲染时,框架会执行受影响的 Composable 函数,这个过程称为重组(Recomposition)。重组并非重新执行整个函数树,而是只执行那些已注册了已改变状态的代码块。
重组的粒度是作用域(Scope)。一个 Composable 函数可以包含多个重组作用域,状态读取发生在哪个作用域,重组就发生在哪个作用域。这使得 Compose 能够精确地只更新必要的 UI 部分,避免全树重绘。
关键点: 重组不是从头到尾重新运行所有代码,而是“增量执行”。只有读取了已变化状态的作用域才会被重新调用,未读取变化状态的 Composable 可以跳过重组。
2.2 跳过重组的条件
为了进一步减少不必要的计算,Compose 编译器利用参数稳定性来推断是否可以安全跳过重组。只有当以下三个条件同时满足时,Composable 才会跳过本次重组:
| 条件 | 说明 |
|---|---|
| 参数声明为稳定(Stable) | 类型被 @Stable 或 @Immutable 标注,或为基本类型/String/函数引用等。 |
| 参数实际值未变化 | 对于稳定类型,编译器通过 equals 或结构相等性判断;对不稳定类型总是视为变化。 |
| Composable 本身不包含副作用 | 例如没有调用 LaunchedEffect 等会破坏跳过逻辑的副作用。 |
如果任何一个条件不满足,Composable 就会无条件重组,即使其输出没有视觉变化。
2.3 参数稳定性:编译器能否信任你
编译器在编译期根据类型的稳定性注解做决策。以下是 Compose 编译器默认认为稳定的类型:
- 所有基本类型(Int、Float、Boolean 等)
- String
- 函数类型(lambda 引用)
- 被
@Immutable或@Stable注解的类
对于自定义数据类,如果不加注解,编译器默认视为不稳定(Unstable),导致即使字段值未变化,也会触发重组。
反直觉案例: 一个 Sealed class 如果不加 @Stable,即使所有子类都是稳定的,编译器仍视整个类为不稳定。必须显式注解。
2.4 实战:不稳定的 Lambda 参数导致频繁重组
下面演示一个典型陷阱:将未缓存的 lambda 作为参数传递,导致外层 Text 每次滑动滑块时都会重组。
@Composable
fun CounterExample() {
var sliderValue by remember { mutableStateOf(0.5f) }
Column {
Slider(value = sliderValue, onValueChange = { sliderValue = it })
// 未缓存 lambda —— 每次重组都生成新实例
MyLabel(getLabel = { format(sliderValue) })
}
}
@Composable
fun MyLabel(getLabel: () -> String) {
// 每次都会重组,因为 lambda 参数不稳定
Text(text = getLabel())
}
优化方案:使用 derivedStateOf 或 remember 缓存计算,使 lambda 保持稳定:
@Composable
fun CounterExampleFixed() {
var sliderValue by remember { mutableStateOf(0.5f) }
val label = remember(sliderValue) { "当前值: ${(sliderValue * 100).toInt()}%" }
Column {
Slider(value = sliderValue, onValueChange = { sliderValue = it })
MyLabel(getLabel = { label })
}
}
// 现在 lambda 引用的是 remember 的字符串,lambda 本身不变(函数引用),Text 可以跳过重组
2.5 重组决策流程
下图展示了 Compose 运行时在重组期间所做的决策树:
2.6 稳定性声明的最佳实践
- 所有作为 Composable 参数的数据类,若其字段都是不可变的(val 且不包含可变集合),请添加
@Immutable注解。 - 对于包含可变状态的类(如使用 mutableStateOf 的类),使用
@Stable注解,并保证equals方法能正确反映内容变化。 - 避免传递未缓存的 lambda:使用
remember或derivedStateOf包装计算逻辑。 - 使用 Compose Compiler 的
--dumptypes调试参数查看哪些类型被编译器判定为不稳定。
2.7 性能影响与调试
不必要的重组直接影响帧率与电量。使用 Layout Inspector 的 Compose 调试模式可以高亮显示重组的 Composable(黄色闪烁)。如果发现高频重组,优先检查:
- 所有传参是否稳定(尤其是 lambda 和集合)。
- 是否在重组作用域内读取了频繁变化的大状态。
- 是否有父 Composable 未正确使用
remember导致子项丢失稳定性。
常见错误: 在 data class 中混用 var 和
MutableList 等可变容器,且未添加 @Stable,导致每次读取都认为实例变化。应使用 val + immutableListOf 并添加 @Immutable。