关于 diagnosing-compose-stability
面向 Claude 的技能,用于诊断 Jetpack Compose 可组合项为何无法跳过。它引导开发者检查 release 构建的编译器报告,定位不稳定参数并确定问题优先级,再交由独立技能修复。
主要功能
- 01为 release 构建启用 Compose Compiler 报告
- 02检查可组合项的可跳过状态与参数稳定性
- 03从类报告追溯不稳定类型的原因
- 04结合热路径与运行时测量排列问题优先级
- 05整理问题清单并交由修复技能处理
适用场景
- 01排查过度重组或丢帧
- 02查明可组合项无法跳过的原因
- 03审查性能敏感 UI 的稳定性依据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ compose-performance-skills --skill diagnosing-compose-stability安装后在 Claude Code 中输入/diagnosing-compose-stability确认已加载。
附带文件references/reading-classes-txt.mdreferences/reading-composables-txt.md
详细介绍
诊断 Compose 稳定性——先查看编译器报告
Compose 会通过比较参数来跳过重组。参数不稳定时,跳过功能会被禁用——本技能会告诉 Claude 如何找出哪些参数不稳定,以及原因。输出是一份按优先级排列的不稳定类型和不可跳过的可组合项列表;修复方法见 ../stabilizing-compose-types/SKILL.md。
何时使用此技能
- 开发者询问“为什么会发生重组?”,报告卡顿、丢帧或滚动不流畅。
@TraceRecomposition日志显示的重组次数超过了有意义的状态变化次数。- 开发者提到 Compose Compiler Reports、
classes.txt、composables.txt、composables.csv、module.json或“不可跳过”。 - 开发者询问如何查找不稳定参数,或
List<Foo>、LocalDateTime或某种领域类型是否稳定。 - 代码审查者要求提供证据,证明对性能敏感的可组合项可以跳过。
何时不使用此技能
- 已知不稳定类型——直接转到
../stabilizing-compose-types/SKILL.md。 - 问题是读取了错误阶段的状态(
Modifier.alpha(state.value));请使用../../recomposition/deferring-state-reads/SKILL.md。 - 问题是误用了
derivedStateOf;请使用../../recomposition/choosing-derivedstateof/SKILL.md。 - 开发者希望通过 CI 实施门禁,而不是进行一次性诊断;请使用
../enforcing-stability-in-ci/SKILL.md。
前置条件
- 使用 Kotlin 2.0.0+,并应用 Compose Compiler Gradle 插件:
id("org.jetbrains.kotlin.plugin.compose")。2.0 之前的kotlinCompilerExtensionVersion流程已废弃。 - 目标模块必须有可构建的 release 变体。报告必须在 release 模式下生成;debug 会添加 Live Literals,使常量看起来像动态值,并扭曲所有报告。
- 应用
org.jetbrains.kotlin.plugin.composeGradle 插件(Kotlin 2.0+)。composeCompiler { … }扩展由该插件提供;AGP 版本无关紧要。 - 可选但**推荐:**开发者的目标应是建立稳定性基线,而不是追求 100% 可跳过(skydoves 热门观点 #1——可跳过性是诊断指标,不是 KPI)。
工作流
- 1. 在模块的
build.gradle.kts中启用报告。 仅限 release 构建——debug 构建会产生误导性数据。
// app/build.gradle.kts (or any compose module)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
composeCompiler {
// Only emit reports for release to avoid Live Literals noise.
val isReleaseBuild = providers.gradleProperty("composeCompilerReports").orNull == "true"
if (isReleaseBuild) {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
}
或使用始终启用的形式:
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
// Optional — opt mutable third-party types into stability:
stabilityConfigurationFiles.add(
rootProject.layout.projectDirectory.file("stability_config.conf")
)
}
- 2. 构建 release 变体,以排除 Live Literals 和解释模式产生的噪声:
./gradlew :app:assembleRelease -PcomposeCompilerReports=true
对于库模块:./gradlew :feature-feed:assembleRelease。必须使用 release flavor——见下文的 WRONG/RIGHT。
- 3. 找到四个输出文件。 它们写入
<module>/build/compose_compiler/:
app/build/compose_compiler/
├── app_release-classes.txt # per-class stability
├── app_release-composables.txt # per-composable signatures
├── app_release-composables.csv # CSV mirror of the above (CI-friendly)
└── app_release-module.json # aggregate counts
如果缺少任何文件,说明插件未针对该变体运行——请重新检查 composeCompiler { reportsDestination = ... } 是否配置在正确的模块中,以及构建是否为 release。
-
4. 首先打开
composables.txt。 这是信号最强的文件。搜索未跟在skippable后面的restartable行。每一行都表示一个无法跳过的重组入口点。在每个代码块中,查看各参数的前缀:stable、unstable、@static、@dynamic。任何unstable参数都会阻止跳过。完整语法见references/reading-composables-txt.md。 -
5. 打开
classes.txt,了解类为何不稳定。 对于composables.txt中标记为unstable的每种类型,在classes.txt中找到其声明。该行会说明原因是var字段、泛型参数还是不稳定的嵌套类型。runtime stable class Box { stable val value: T }这种形式表示“仅当替换后的 T 的运行时$stable: Int字段表明其稳定时,此类才稳定”——见references/reading-classes-txt.md。 -
6. 打开
module.json查看排查数据。 其中包含可跳过的可组合项、可重启的可组合项、稳定类等的数量。可用于比较修复前后的情况,或决定优先处理哪个模块。切勿将这些数量视为目标——它们用于发现回归,而不是追求 100%。 -
7. 按热点路径确定优先级。 启动时只运行一次的不可跳过可组合项无关紧要;位于
LazyColumn项内的则至关重要。修复前,请结合测量数据进行交叉验证(使用 skydoves/compose-stability-analyzer 的@TraceRecomposition,或 Macrobenchmark 的FrameTimingMetric)——见../../measurement/tracing-recompositions-at-runtime/SKILL.md。 -
8. 移交给修复技能。 列出不稳定类型和有问题的可组合项,并应用
../stabilizing-compose-types/SKILL.md。
模式
模式:HighlightedSnacks — 解读不可跳过的可组合项
编译器报告会直接在函数签名中呈现原因。通过这个真实示例向开发者说明。
restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int,
unstable snacks: List<Snack>, // <-- blocks skipping
stable onSnackClick: Function1<Long, Unit>,
)
诊断步骤:
- 函数是
restartable,但没有skippable前缀。因此,每当其父项重组时,它都会重组。 - 原因是
unstable snacks: List<Snack>参数。kotlin.collections.List是接口;编译器无法证明其实现不可变。 - 打开
classes.txt并查找Snack。如果Snack本身是unstable,先修复数据类;如果它是stable,那么问题仅在List包装类型。 - 移交给修复技能:将
List<Snack>替换为kotlinx.collections.immutable.ImmutableList<Snack>,或在开发者认可这一约定的前提下,将kotlin.collections.*添加到stability_config.conf。
模式:“stable but runtime”——runtime 的含义
runtime stable class Box {
stable val value: T
}
这并不表示不稳定。编译器会在运行时生成合成的 $stable: Int 字段,并在组合过程中查询它。仅当替换后的 T 报告为稳定时,此类才稳定。切勿使用 @Stable 注解 runtime 类来“提升”其稳定性——运行时检查才是正确机制。
模式:错误与正确——运行构建
# WRONG
./gradlew :app:assembleDebug
# WRONG because: debug enables Live Literals; constant 0 dp becomes a getter, every literal looks dynamic, and counts in module.json drift versus what ships to users.
# RIGHT
./gradlew :app:assembleRelease -PcomposeCompilerReports=true
模式:错误与正确——读取报告文件
// WRONG — reading composables.txt without checking the leading flags
fun MyScreen(...)
// WRONG because: skipping the `restartable`/`skippable` prefix discards the only data point that determines whether unstable params actually cost anything.
// RIGHT — every diagnosis quotes the full prefix and per-param annotations
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MyScreen(
stable user: User,
stable onClick: Function0<Unit>,
)
模式:输出目录为空
如果发布构建后 build/compose_compiler/ 缺失或为空:
- 确认此模块已应用
org.jetbrains.kotlin.plugin.compose插件——该扩展是按模块配置的。 - 确认构建确实编译了 Kotlin 源码(而不是因任务已是最新而未执行)。修改一个文件,或运行
./gradlew :app:clean :app:assembleRelease。 - 确认
reportsDestination设置在composeCompiler { }块内,而不是通过旧版kotlinOptions的 freeCompilerArgs 流程设置。
强制规则
- MUST 构建 release 变体。Debug 构建会生成 Live Literals,让常量看起来像动态值,并扩大报告中“unstable”的范围。
- MUST 阅读
composables.txt中按参数列出的注解(stable/unstable/@static/@dynamic),不能只看函数名。标志前缀就是诊断依据。 - MUST 将不稳定参数交叉对照
classes.txt,以确定根本原因(var、泛型、不稳定的字段类型或接口)。 - MUST NOT 追求 100 percent 的可跳过性。Skydoves 的犀利观点 #1:可跳过性是诊断指标,不是 KPI。启动时只运行一次的可组合函数不需要具备可跳过性。
- MUST NOT 仅凭报告就将类标注为
@Stable——该决定应由修复技能做出,并由其评估契约。 - MUST NOT 只读取
composables.csv的行数就认为工作已完成。这些计数是回归哨兵;逐行注解才是真正的诊断依据。 - PREFERRED: 通过
../enforcing-stability-in-ci/SKILL.md,在 CI 中接入相同的标志(skydovescompose-stability-analyzer插件或社区的ComposeGuard插件),以便在 PR 审查中发现回归。 - PREFERRED: 为每次发布记录一个基线
module.json,以便通过差异发现未来的回归。
验证
- 应用
org.jetbrains.kotlin.plugin.compose插件后,./gradlew :app:assembleRelease(或针对特定模块的构建)成功完成。 - 以下四个文件均存在:
<module>_release-classes.txt、<module>_release-composables.txt、<module>_release-composables.csv、<module>_release-module.json。 - 至少具备以下之一:从
composables.txt复制的不稳定参数位置列表,或来自module.json、确认计数为零并证明无需修复的结果。 - 已将每个不稳定参数追溯到
classes.txt中的一个类,以便修复技能获得具体的根本原因(var、List、LocalDateTime等)。 - 开发者理解,
runtime stable class …不是问题——这是编译器正确发出的惰性稳定性信息。
参考资料
- Android Developers — 诊断稳定性:https://developer.android.com/develop/ui/compose/performance/stability/diagnose
- Android Developers — 稳定性概览:https://developer.android.com/develop/ui/compose/performance/stability
- Compose Compiler 发行说明:https://developer.android.com/jetpack/androidx/releases/compose-compiler
- Ben Trengrove — Jetpack Compose 稳定性详解:https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8
- Chris Banes — 可组合函数指标:https://chrisbanes.me/posts/composable-metrics/
- 为什么要在 release 版本中测试性能:https://medium.com/androiddevelopers/why-should-you-always-test-compose-performance-in-release-4168dd0f2c71
- skydoves — 通过掌握稳定性优化应用性能:https://medium.com/proandroiddev/optimize-app-performance-by-mastering-stability-in-jetpack-compose-69f40a8c785d
- skydoves — compose-stability-analyzer:https://github.com/skydoves/compose-stability-analyzer
references/reading-classes-txt.md— classes.txt 的完整语法及示例(stable / unstable / runtime / generic)。references/reading-composables-txt.md— composables.txt 的完整语法,包括 restartable、skippable、readonly、scheme,以及按参数列出的stable/unstable/@static/@dynamic。