用途
Compose stability diagnostics 用于处理 Jetpack Compose 中由参数引起的性能问题:判断输入在重组之间能否以廉价且可预测的方式比较。它帮助开发者识别不稳定参数、非可跳过 composable、不稳定的 UI 状态类、集合参数以及 Kotlin 编译器报告相关问题。
核心原则
Compose 参数性能问题的关键在于输入是否能在重组之间廉价且可预测地比较。在 Kotlin 2.0.20+ 中,strong skipping 默认启用,因此不稳定参数不再自动使可重启(restartable)composable 变为不可跳过(non-skippable)。但这并不意味着稳定性无关紧要:不稳定参数按实例标识(===)比较,稳定参数按相等性(equals)比较,频繁变化的实例仍可能破坏跳过。
使用该技能时,应先确定所处的编译器模式,再在该上下文中阅读报告。
何时使用
- 某个 composable 或屏幕的重组次数超出预期,怀疑是参数频繁变化。
- 传给 composable 的 UI 状态/模型类包含 List、Set、Map、range、Java time/money 类型或第三方类型。
- composables.txt / classes.txt 显示不稳定参数或不可跳过的 composable。
- 项目使用 Kotlin < 2.0.20、禁用了 strong skipping,或参考的是旧的 Compose 编译器报告指南。
1. 从 strong skipping 开始
在 Kotlin 2.0.20+ 上,strong skipping 默认启用。在该模式下:
- 可重启 composable 即使参数不稳定也是可跳过的,除非显式选择退出。
- 稳定参数用 equals 比较。
- 不稳定参数用实例相等性(===)比较。
- composable 内的 lambda 会根据捕获自动 remember。
这意味着问题从「这个 composable 到底能不能跳过?」变为「这些参数会按我预期的方式比较吗,调用方是否每帧都在创建新的不稳定实例?」
对于较旧的编译器设置或禁用了 strong skipping 的情况,旧规则仍然重要:带不稳定参数的可重启 composable 可能可重启但不可跳过。
2. 生成编译器报告
在 Kotlin 2.0+ 中,Compose Compiler 通过 Kotlin Gradle 插件配置:
plugins {
alias(libs.plugins.android.application) // or android.library / jvm
alias(libs.plugins.kotlin.android) // or kotlin.multiplatform / kotlin.jvm
alias(libs.plugins.compose.compiler)
}
if (providers.gradleProperty("composeReports").orNull == "true") {
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
}
然后构建你关心的编译器配置对应的变体,例如:
./gradlew :app:assembleRelease -PcomposeReports=true
运行时性能分析请使用 release/非 debuggable 构建。编译器报告是构建期输出,因此重要的是匹配你实际发布的变体和编译器标志。
关键文件:
| 文件 | 说明 |
|---|---|
<module>-classes.txt |
类和属性的稳定性 |
<module>-composables.txt |
可重启/可跳过状态及参数稳定性 |
<module>-composables.csv |
同样数据,可排序形式 |
<module>-module.json |
聚合指标 |
3. 在语义需要处修复稳定性
选择最轻量的修复,使类型的不可变性或相等性语义成立。
不可变集合
kotlin.collections.List 是接口;Compose 无法知道运行时实现是否不可变。在 UI 状态边界优先使用 kotlinx.collections.immutable:
// Before: unstable collection interfaces
data class UiState(val items: List<Item>, val tags: Set<String>)
// After: immutable collection contracts
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
data class UiState(val items: ImmutableList<Item>, val tags: ImmutableSet<String>)
生产者在边界处用 .toImmutableList() / .toImmutableSet() 转换一次。
@Immutable / @Stable
- 当每个属性实际上不可变,且相等性描述了所有可观察状态时,使用
@Immutable。 - 对于可变状态可被 Compose 观察到的类型(通常通过 MutableState),使用
@Stable。 - 不要为了消除报告而加注解。错误的稳定性承诺可能导致 UI 陈旧。
第三方不可变类型
对于无法注解的类型,使用 stabilityConfigurationFiles:
composeCompiler {
stabilityConfigurationFiles.add(
rootProject.layout.projectDirectory.file("compose_stability.conf"),
)
}
java.math.BigDecimal
java.math.BigInteger
java.time.*
kotlinx.datetime.*
只列出你愿意承诺不可变的类型。不要列出可变类型,例如 java.util.Date。
快速参考
| 症状 | 诊断 | 修复 |
|---|---|---|
| Kotlin 2.0.20+ 但旧文档说不稳定即不可跳过 | strong skipping 改变了默认行为 | 改为检查比较语义和实例频繁变化 |
unstable val items: List<Item> |
接口集合 | 使用 ImmutableList<Item> 或其他真正不可变的包装 |
unstable val price: BigDecimal |
外部不可变类型 | 加入 stability config |
对含可变内部状态的类型加 @Immutable |
错误承诺 | 修复模型或移除注解 |
| 尽管有 strong skipping,composable 仍跳过不佳 | 每次重组都产生新的不稳定实例 | remember、提升(hoist),或让类型稳定/基于相等性 |
| 报告未生成 | 缺少 Compose 编译器插件或未设置标志 | 应用 org.jetbrains.kotlin.plugin.compose 并启用目标目录 |
何时不适用
- 问题是组合中快速变化的 State 读取,例如滚动或动画。应使用 compose-state-deferred-reads。
- 重组次数与实际数据变化相符。
- 缺陷是数据错误或状态陈旧,而非多余工作。
- 代码仅用于测试,可读性比报告整洁更重要。
相关
- compose-state-deferred-reads:帧率相关的状态通常应在 layout/draw 而非 composition 中读取。
- compose-recomposition-performance:当不确定涉及哪个重组维度时的入口点。