重组基本原理与性能影响

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 运行时在重组期间所做的决策树:

重组跳过决策流程 重组触发 参数是否稳定? 不稳定 稳定 值改变? 重组 跳过重组 说明:稳定+值未变化 → 跳过重组,其他情况均会触发重组。 优化目标:确保上游 Composable 的参数都是稳定的,避免不必要的重组。

2.6 稳定性声明的最佳实践

  • 所有作为 Composable 参数的数据类,若其字段都是不可变的(val 且不包含可变集合),请添加 @Immutable 注解。
  • 对于包含可变状态的类(如使用 mutableStateOf 的类),使用 @Stable 注解,并保证 equals 方法能正确反映内容变化。
  • 避免传递未缓存的 lambda:使用 remember 或 derivedStateOf 包装计算逻辑。
  • 使用 Compose Compiler 的 --dumptypes 调试参数查看哪些类型被编译器判定为不稳定。

2.7 性能影响与调试

不必要的重组直接影响帧率与电量。使用 Layout Inspector 的 Compose 调试模式可以高亮显示重组的 Composable(黄色闪烁)。如果发现高频重组,优先检查:

  1. 所有传参是否稳定(尤其是 lambda 和集合)。
  2. 是否在重组作用域内读取了频繁变化的大状态。
  3. 是否有父 Composable 未正确使用 remember 导致子项丢失稳定性。
常见错误: 在 data class 中混用 var 和 MutableList 等可变容器,且未添加 @Stable,导致每次读取都认为实例变化。应使用 val + immutableListOf 并添加 @Immutable。