关于 compose-stability-diagnostics
面向 Jetpack Compose 开发者的诊断指南,帮助依据编译器报告和运行时证据减少不必要的重组。它区分稳定性语义、实例变化与调用点输入导致的问题。
主要功能
- 01理解强跳过的比较规则
- 02生成并检查编译器报告
- 03诊断不稳定参数与实例变化
- 04稳定延迟列表项输入
- 05通过重复测量验证修复
适用场景
- 01调查界面交互期间的重组
- 02检查可组合项未跳过的原因
- 03减少延迟列表项 Lambda 引发的重组
- 04发布前验证稳定性调整
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ chrisbanes/ skills --skill compose-stability-diagnostics安装后在 Claude Code 中输入/compose-stability-diagnostics确认已加载。
详细介绍
Compose 稳定性诊断
核心原则
Compose 参数修复应以证据为起点。先确定编译器模式和参数比较行为,再修改真正导致无法跳过重组的模型或调用点。
Kotlin 2.0.20+ 默认启用强跳过。不稳定参数不再自动导致可重启的可组合项无法跳过,但不稳定参数会按实例身份(===)比较,而稳定参数会按相等性(equals)比较。频繁变化的不稳定实例仍可能导致无法跳过重组。
诊断流程
- 确认症状:重组次数、编译器报告输出,或疑似频繁变化的参数。
- 确定编译器模式:Kotlin/Compose Compiler 版本,以及是否启用了强跳过。
- 生成或查看已发布变体的 Compose Compiler 报告。
- 对每个可疑参数,判断问题出在稳定性语义、实例频繁变化,还是调用方创建了 lambda/派生值。
- 应用最轻量的修复,使类型或调用点如实反映其行为。
- 在相同交互中重新测量,或重新查看同一份报告,然后再声称问题已修复。
1. 首先理解强跳过
在 Kotlin 2.0.20+ 中,强跳过默认启用。在这种模式下:
- 即使参数不稳定,可重启的可组合项仍可跳过重组,除非显式选择不启用。
- 稳定参数使用
equals比较。 - 不稳定参数使用实例相等性(
===)比较。 - 可组合项内的 lambda 会根据捕获内容自动记忆化。
请问:“这些参数的比较方式是否符合预期?调用方是否每帧都会创建新的不稳定实例?”
对于较旧的编译器配置或禁用了强跳过的情况,旧规则仍然适用:带有不稳定参数的可重启可组合项可以重启,但无法跳过重组。
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/非调试构建。编译器报告是构建时输出,因此关键是确保使用与你发布时相同的变体和编译器标志。
关键文件:
| 文件 | 用途 |
|---|---|
<module>-classes.txt | 类和属性的稳定性 |
<module>-composables.txt | 可重启/可跳过状态和参数稳定性 |
<module>-composables.csv | 相同数据的可排序形式 |
<module>-module.json | 汇总指标 |
3. 只修复已证实的参数问题
选择能如实反映类型不可变性或相等性语义的最轻量修复。
不可变集合
如果报告显示 UI 状态使用了集合接口,优先在 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 等可变类型。
4. 稳定惰性项输入
如果惰性项重组是由调用点频繁变化引起的,应稳定传递给每个项的值,而不是盲目地给模型添加注解。
将每项生命周期内保持稳定的输入提升到外部并记住:
// ❌ BAD — new lambda instances when parent recomposes
items(list, key = { it.id }) { item ->
RowCard(
onClick = { onItemClick(item.id) },
isHighlighted = { item.id == selectedId },
)
}
// ✅ GOOD — stable captures for this item instance
items(list, key = { it.id }) { item ->
val onClick = remember(item.id) { { onItemClick(item.id) } }
val isHighlighted = remember(item.id, selectedId) { item.id == selectedId }
RowCard(onClick = onClick, isHighlighted = isHighlighted)
}
如果行位置信息(isFirst、isLast、圆角半径)仅取决于索引,也应使用 remember(index) { … } 将其提升到外部。但不要指望仅靠这一点就能修复回写或跨行测量问题。
提升后,通过重组次数断言验证焦点移动和插入行为。
快速参考
| 症状 | 诊断 | 修复方法 |
|---|---|---|
| 使用 Kotlin 2.0.20+,但旧文档说不稳定就意味着不可跳过 | 强跳过改变了默认行为 | 改为检查比较语义和实例是否频繁变化 |
unstable val items: List<Item> | 接口集合 | 使用 ImmutableList<Item> 或其他真正不可变的封装类型 |
unstable val price: BigDecimal | 外部不可变类型 | 将其添加到稳定性配置中 |
对含有可变内部状态的类型使用了 @Immutable | 做出了不实承诺 | 修复模型或移除注解 |
| 尽管启用了强跳过,可组合项仍无法有效跳过 | 每次重组都会创建新的不稳定实例 | 使用 remember、提升到外部,或让类型稳定/基于相等性进行比较 |
| 即使数据未变化,父项重组时惰性项也会重组 | 父项每次重组都会创建新的 Lambda 或派生值实例(§4) | 使用 remember(item.id) { … } 将每项的值提升到外部 |
| 未生成报告 | 缺少 Compose 编译器插件或未设置标志 | 应用 org.jetbrains.kotlin.plugin.compose 并启用报告目标 |
何时不应应用
- 问题是跨阶段回写或跨行测量读取。请使用
compose-state-deferred-reads。 - 问题是在组合阶段读取频繁变化的
State,例如滚动或动画。请使用compose-state-deferred-reads。 - 重组次数与实际数据变化相符。
- Bug 是数据错误或状态过期,而不是工作量过多。
- 代码仅用于测试,可读性比报告整洁更重要。
相关内容
compose-state-deferred-reads- 帧率较高的状态通常应在布局/绘制阶段而非组合阶段读取。compose-recomposition-performance- 如果不确定涉及哪个重组维度,请从此入口开始。