重组触发机制:重组条件、重组范围、重组粒度

原理概述

Compose 的重组(Recomposition)是框架在观察到的状态发生变化后,重新执行可组合函数以更新 UI 的过程。触发重组的核心条件是:可观察状态(State)发生改变,且该状态被当前组合范围内的可组合函数读取。Compose 通过快照系统(Snapshot System)追踪状态读取,并仅在必要的最小范围内重新执行代码。

重组触发条件

条件类型 具体场景 重组范围
状态变化 mutableStateOf、StateFlow、LiveData 等可观察对象的值改变 所有读取该状态的 Composable 及调用链
重组回传 父 Composable 重组导致子 Composable 参数变化 直接受影响的子 Composable
隐式触发 使用 remember 但 key 变化;derivedStateOf 依赖变化;LaunchedEffect key 变化 仅该 Composable 内部逻辑
非重组副作用 SideEffect、DisposableEffect 的生命周期回调 不触发 UI 重组,仅执行副作用

重组范围与粒度

Compose 不会重组整个界面,而是以可组合函数为最小粒度进行“跳过”或“执行”。重组范围由状态读取的 读取作用域(Read Scope)决定:

  • 函数级粒度:只要某个 Composable 内读取了某个状态,该 Composable 整体会被标记为“需要重组”。
  • 跳过机制:参数未变的 Composable 如果被标记为 @NonRestartableComposable 或参数稳定(Stable)则跳过重组。
  • 最小化范围:通过 derivedStateOf 或 keys 可进一步收窄重组范围。

优化策略

  1. 拆分细粒度 Composable:将频繁变化的部分拆成独立 Composable,避免父级大范围重组。
  2. 使用 remember 缓存稳定对象:例如 remember { mutableStateOf(...) } 避免每次重组重新创建。
  3. 避免在重组中执行昂贵计算:移到 derivedStateOf 或 LaunchedEffect 中。
  4. 标记稳定类型:对数据类使用 @Stable 或 @Immutable 注解,帮助 Compose 跳过不必要的重组。
  5. 使用 key 参数:在 LazyColumn 等列表组件中提供唯一 key,减少全量重组。

关键守则:重组 ≠ 重新绘制。重组只重建 UI 树结构,不一定会导致重绘。优化目标是减少重组次数,而非重绘次数。

代码实现示例

以下示例展示不同粒度的重组触发:

@Composable
fun CounterExample() {
    var count by remember { mutableStateOf(0) }
    // 每次 count 变化,整个 CounterExample 重组
    Column {
        Text("Count: $count")
        // 独立子 Composable,仅在内部读取状态时重组
        IncrementButton(onClick = { count++ })
    }
}

@Composable
fun IncrementButton(onClick: () -> Unit) {
    // 这里未读取任何状态,因此永远不会因 count 变化而重组
    Button(onClick = onClick) {
        Text("+1")
    }
}

调试工具

  • Compose Compiler Metrics:在 build.gradle 中配置 composeCompilerReports 和 composeCompilerMetrics,输出重组频率和稳定类型报告。
  • Layout Inspector:Android Studio 内置工具,实时查看 Composable 重组次数。
  • RecompositionCount:通过 Recomposer 或自定义调试工具统计。

在 Gradle 中添加以下配置以生成 Compiler Metrics:

android {
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.4"
    }
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
    kotlinOptions {
        freeCompilerArgs += [
            "-P",
            "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
            project.buildDir.absolutePath + "/compose-reports"
        ]
        freeCompilerArgs += [
            "-P",
            "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
            project.buildDir.absolutePath + "/compose-metrics"
        ]
    }
}

常见错误

  • 在非协程中直接修改状态:导致同步触发重组,可能引发卡顿。应使用 snapshotFlow 或协程。
  • 将可变对象(如 List)存入状态:修改对象内部属性不会触发重组。应使用不可变复制或 mutableStateListOf。
  • 不必要的全局状态:将多个不相关状态放在同一个 ViewModel 中,导致局部变化引发大范围重组。

性能验证方法

  • 使用 Compose Compiler Metrics 报告:检查每个 Composable 的 restartable 和 skippable 标记。
  • 帧率监控:通过 Profile GPU Rendering 或 Systrace 观察帧时间。
  • 手动添加日志:在 Composable 开头打印日志,观察调用次数。

重组触发流程 SVG 图

重组触发流程 状态变化 读取该状态? 不重组 标记重组 执行重组 只有读取状态的函数才会触发