关于 tracing-recompositions-at-runtime
此技能介绍如何用 `skydoves/compose-stability-analyzer` 的编译期插桩诊断 Jetpack Compose 的过度重组。Android 开发者可在 logcat 查看参数与状态变化,也可使用 IDE 热图,并在接近发布的构建中追踪。
主要功能
- 01编译期 `@TraceRecomposition` 插桩
- 02参数和状态变化差异
- 03基于 logcat 的重组追踪
- 04可选的 IDE 重组热图
- 05通过运行时开关控制输出
适用场景
- 01调查调试版与发布版重组次数差异
- 02在接近发布的构建中验证稳定性修复
- 03查找重组背后的参数或状态变化
- 04按 SLO 跟踪组合项重组次数
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ compose-performance-skills --skill tracing-recompositions-at-runtime安装后在 Claude Code 中输入/tracing-recompositions-at-runtime确认已加载。
详细介绍
在运行时追踪重组 — @TraceRecomposition、logcat 和实时热力图
Layout Inspector 会统计重组次数并显示参数变化原因,但它仅适用于调试模式,因为 Live Literals 和解释执行的 Compose 运行时会抬高统计数值。skydoves/compose-stability-analyzer 中的 @TraceRecomposition 会在编译时为可组合项插桩,并将每次重组的差异(哪个状态发生了变化、值如何变化)输出到 Recomposition 标签下的 logcat。开发者启用该插桩后,它可用于任何构建,包括带调试符号的 release 构建;还可为 IntelliJ / Android Studio 插件的实时重组热力图提供数据。
此技能是 ../../recomposition/debugging-recompositions/SKILL.md 的发布模式补充方案。Layout Inspector 仅适用于调试模式,设置快速,适合初步排查。@TraceRecomposition 用于确认实际情况:为可疑的可组合项插桩,构建带 R8 的 release 版开发 APK,运行用户操作流程,然后查看每次重组的日志行。
何时使用此技能
- 某个可组合项的重组次数超出预期,而 Layout Inspector 的统计结果无法得出结论(调试版和发布版的统计数不同,或可疑项是 Layout Inspector 未覆盖的内联可组合项)。
- 需要在合并前,针对等同于发布版的构建确认稳定性或强跳过修复。
- 开发者希望直接查看逐状态、逐参数的变化差异,而不是在 Layout Inspector 树中逐项点击。
- 团队希望为可组合项建立重组次数基线,作为 SLO(“PriceTicker 每次价格更新最多重组 1 次;绝不因父级更新而重组”)。
- 用户提到
@TraceRecomposition、“trace recomposition”、“compose-stability-analyzer”、“recomposition logcat”、“recomposition heatmap”或“release-mode recomposition”。
何时不使用此技能
- 开发者只想查看调试模式下的重组次数——Layout Inspector 设置更快。请参阅
../../recomposition/debugging-recompositions/SKILL.md。 - 构建是生产发布版,且无诊断需求——必须关闭插桩。请参阅下文的运行时开关模式。
- 团队需要一个能阻止未来稳定性回退的 CI 门禁——运行时追踪用于诊断;门禁方案是
../../stability/enforcing-stability-in-ci/SKILL.md(stabilityCheck)。 - 需求是测量每帧耗时或端到端的用户感知性能,而非重组次数——请使用
../generating-baseline-profiles/SKILL.md中的MacrobenchmarkRule+FrameTimingMetric。
前提条件
- 在包含待插桩可组合项的模块中添加 Gradle 插件
com.github.skydoves.compose.stability.analyzer(最新版本,v0.7.3+)。 - 在清单中声明一个
Application子类,以便从onCreate()调用ComposeStabilityAnalyzer.setEnabled(...)。 - 同一模块可访问 Compose Compiler——已应用
org.jetbrains.kotlin.plugin.compose(Kotlin 2.0+)。 - 提供运行时开关可读取的
BuildConfig字段或功能标志。BuildConfig.DEBUG可用;对于生产风格的性能分析构建,建议使用自定义的BuildConfig.ENABLE_RECOMPOSITION_TRACE。 - 要使用 IntelliJ / Android Studio 热力图:从 JetBrains marketplace 安装
compose-stability-analyzer插件(或从 GitHub 仓库在本地构建)。 - 熟悉
../../recomposition/debugging-recompositions/SKILL.md,以便开发者在使用运行时追踪前,已在调试模式下确定可疑的可组合项。
工作流
1. 应用 Gradle 插件
在模块的 build.gradle.kts 中:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
alias(libs.plugins.compose.stability.analyzer)
}
在 gradle/libs.versions.toml 中:
[versions]
composeStabilityAnalyzer = "0.7.3"
[plugins]
compose-stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version.ref = "composeStabilityAnalyzer" }
2. 配置分析器
在同一个 build.gradle.kts 中,与插件块并列:
composeStabilityAnalyzer {
enabled.set(true) // compile-time switch; runtime toggle below gates emission
}
enabled.set(true) 控制编译器是否插入检测代码。使用 enabled.set(false) 时,所有 @TraceRecomposition 注解都不会生效。让它在所有构建类型中保持开启没有问题——实际控制生产环境是否启用的是运行时开关。
3. 为要跟踪的可组合项添加注解
import com.skydoves.compose.stability.runtime.TraceRecomposition
@TraceRecomposition(traceStates = true)
@Composable
fun PriceTicker(price: Price) {
Text(price.formatted)
}
traceStates = true 会将差异范围扩展到可组合项主体内对 mutableStateOf 的读取,而不只是参数。首次调查时可设为 true;确定原因后将其改为 false,以保持日志简洁。
4. 在 Application.onCreate() 中添加运行时开关
import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
}
}
若不调用此方法,所有带注解的可组合项仍会向 logcat 输出内容,包括在 release 构建中。请使用 BuildConfig.DEBUG 或自定义的 BuildConfig.ENABLE_RECOMPOSITION_TRACE 控制开关,确保生产 APK 不输出日志。
5. 重现症状并查看 logcat
adb logcat -s Recomposition:D
以下是一个动画 PriceTicker 的示例输出,其价格从 99.0 变为 99.5(仅作示意——日志的具体格式取决于分析器版本):
D/Recomposition: [Recomposition #1] PriceTicker
D/Recomposition: ├─ [param] price: Price changed (Price(99.0) → Price(99.5))
D/Recomposition: [Recomposition #2] PriceTicker
D/Recomposition: ├─ [param] price: Price unchanged (skipped via strong-skipping equals)
# 后的数字是每个实例的计数器,会在可组合项的重启作用域整个生命周期内累计。可组合项只显示在屏幕上两秒,却打印出 [Recomposition #50],这就是明显的问题迹象。
6. 在 Android Studio 中打开实时热力图(可选)
从 JetBrains Marketplace 安装 Compose Stability Analyzer 插件。安装插件并运行应用后,每个带有 @TraceRecomposition 注解的可组合项旁边,编辑器装订线中都会显示一个彩色徽标。参考阈值如下(仅作示意——当前区间请参阅插件的设置面板):
- 绿色——当前会话中的重组次数少于 10 次。
- 黄色——重组次数为 10 到 50 次。
- 红色——重组次数超过 50 次。
点击徽标可打开侧边面板,其中会列出每个 [Recomposition #N] 条目及其差异。该面板与 logcat 内容一致,但会按可组合项实例分组,让开发者无需滚动查看 logcat,就能发现哪个 LazyColumn 行的行为异常。
7. 追溯到上游修复方案
运行时跟踪会指出可组合项及发生变化的参数。修复方案位于其他位置:
- 参数因类型不稳定而导致重组 →
../../stability/diagnosing-compose-stability/SKILL.md和../../stability/stabilizing-compose-types/SKILL.md。 - 参数因捕获的 lambda 或
Flow而导致重组 →../../recomposition/using-strong-skipping-correctly/SKILL.md和../../side-effects/collecting-flows-safely/SKILL.md。 - 在错误阶段(Composition 与 Layout、Draw)读取了状态 →
../../recomposition/deferring-state-reads/SKILL.md。 - Layout Inspector 显示了需要处理的 Argument Change Reason 状态 →
../../recomposition/debugging-recompositions/SKILL.md。
修复合入后,重新运行跟踪;修复后的 logcat 应仅显示一条初始的 [Recomposition #1],同一场景中不再出现后续条目。
8. 在 CI 中防止未来回归
@TraceRecomposition 用于诊断。防止下一次回归则是 CI 的职责:按照 ../../stability/enforcing-stability-in-ci/SKILL.md 中的说明,在 CI 中启用 stabilityCheck;当原本可跳过的可组合项变为不可跳过时,构建会失败。
模式
模式:为可疑项添加注解、运行带调试信息的 release 构建并查看差异
// RIGHT
@TraceRecomposition(traceStates = true)
@Composable
fun PriceTicker(price: Price) {
Text(price.formatted)
}
logcat 输出示例(仅作示意——日志的具体格式取决于分析器版本;价格每秒变化一次;周围的行随每次父项更新而重组一次):
D/Recomposition: [Recomposition #3] PriceTicker
D/Recomposition: ├─ [param] price: Price changed (Price(99.0) → Price(99.5))
D/Recomposition: [Recomposition #4] PriceTicker
D/Recomposition: ├─ [param] price: Price unchanged
D/Recomposition: ├─ [reason] parent restart scope re-invoked; strong-skipping equals matched
第二条记录是理想情况:父项已更新,equals() 检查生效,主体被跳过。
模式:使用构建标志控制运行时开关
// WRONG
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// ComposeStabilityAnalyzer.setEnabled(...) is never called.
// Every annotated composable emits to logcat in every build, including release.
}
}
// WRONG because: shipping with tracing enabled adds logcat I/O on every recomposition,
// which adds nontrivial overhead on hot composables (LazyColumn rows in particular)
// and pollutes user-installed-app logs on shared devices.
// RIGHT
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
}
}
对于带调试符号的 release 性能分析 APK,建议使用专用标志,而不是 BuildConfig.DEBUG:
// RIGHT — explicit profiling flag, decoupled from debug
ComposeStabilityAnalyzer.setEnabled(BuildConfig.ENABLE_RECOMPOSITION_TRACE)
模式:不要在生产版本中发布 @TraceRecomposition 注解
// WRONG
// Production release with @TraceRecomposition still annotated on hot composables and
// ComposeStabilityAnalyzer.setEnabled(true) hard-coded in Application.
// WRONG because: logcat I/O on every recomposition adds nontrivial overhead on hot
// composables. The instrumentation also captures parameter values into log strings,
// which can leak PII if a composable receives a user model.
// RIGHT — annotation present, runtime gate keeps it dormant in release
@TraceRecomposition(traceStates = true)
@Composable
fun PriceTicker(price: Price) { Text(price.formatted) }
// Application:
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG) // dormant in release
模式:查明原因后,调低 traceStates
// First investigation — verbose
@TraceRecomposition(traceStates = true)
@Composable
fun Feed(state: FeedState) { /* ... */ }
// Cause identified, fix shipped, keep the annotation as a tripwire — quieter
@TraceRecomposition(traceStates = false)
@Composable
fun Feed(state: FeedState) { /* ... */ }
traceStates = true 会记录函数体内每次 mutableStateOf 读取状态的变化。首次排查时,这些信息非常有价值;查明原因后,就会变成噪声。切换为 false 后,仍会保留每次重组的计数器(仍可用作预警),但不再记录每个状态的差异。
模式:将运行时跟踪与 CI 稳定性门禁配合使用
@TraceRecomposition 能发现开发者当前正在追查的回归问题,却无法应对队友下周引入的回归问题。将它与 stabilityCheck 配合使用:
# RIGHT — both layers in place
- @TraceRecomposition annotates suspect composables; runtime toggle gated on BuildConfig.DEBUG.
- ./gradlew :app:stabilityCheck runs in CI per ../../stability/enforcing-stability-in-ci/SKILL.md.
- The .stability baseline updates only after a deliberate review.
# WRONG — diagnosis without prevention
- @TraceRecomposition is the only mechanism in place.
- Next PR introduces an unstable type; nothing in CI catches it; the perf regression
ships and is found again at runtime weeks later.
强制规则
- 必须根据构建配置字段(
BuildConfig.DEBUG或专用的BuildConfig.ENABLE_RECOMPOSITION_TRACE)或功能标志来控制ComposeStabilityAnalyzer.setEnabled(...)。绝不能将setEnabled(true)硬编码。 - 必须将运行时跟踪与
../../stability/enforcing-stability-in-ci/SKILL.md中的stabilityCheckCI 门禁结合使用。运行时跟踪用于诊断;CI 门禁用于防止下一次回归。两者缺一不可,否则工作流就不完整。 - 不得在启用了
@TraceRecomposition插桩的情况下发布生产版本。注解可以保留在可组合项上,但生产构建中的ComposeStabilityAnalyzer.setEnabled(...)必须解析为false。 - 不得在缺少上下文时,将
[Recomposition #N]计数视为严格的 SLO(什么场景?哪台设备?发布版还是调试版?)。应改为跟踪固定场景中的计数变化——“修复后,价格行情场景产生 1 条记录;修复前为 30 条”。 - 必须在报告发现时同时指出可组合项和变化的参数(“
PriceTicker会随父项每次更新而重组,因为price被报告为 Changed,但Price数据类是@Immutable,且equals()应该能匹配”)。“它经常重组”不算发现。 - **推荐:**首次调查时从
traceStates = true开始(日志更丰富);查明原因后设为false,让跟踪成为更安静的预警机制。 - **推荐:**安装 IntelliJ / Android Studio 插件,以查看装订线热力图(示例区间:绿色 <10、黄色 10–50、红色 50+——请根据插件当前设置确认)。它能显示哪个可组合项有问题,无需搜索 logcat。
- **推荐:**仅在少数经过审慎选择的可组合项上保留
@TraceRecomposition注解(屏幕中频繁更新的可组合项、LazyColumn行可组合项)。给所有可组合项都加上注解,会破坏热力图的信噪比。
验证
- 已将
com.github.skydoves.compose.stability.analyzer插件应用于拥有待插桩可组合项的模块。 - 已配置
composeStabilityAnalyzer { enabled.set(true) }。 -
Application.onCreate()调用了ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)(或通过其他构建标志控制)。该调用未将true硬编码。 - 至少有一个可组合项标注了
@TraceRecomposition(traceStates = true)。 - 重现该场景时,
adb logcat -s Recomposition:D会打印[Recomposition #N] <ComposableName>行。 - 每条输出行都指出一个参数或状态,并报告
changed (oldValue → newValue)或unchanged。 - 在发布构建中(或关闭门禁后),
adb logcat -s Recomposition:D不会打印任何内容——插桩处于休眠状态。 - 已按照
../../stability/enforcing-stability-in-ci/SKILL.md配置 CI 稳定性门禁(./gradlew :app:stabilityCheck),以便在下一次回归发布前将其捕获。
参考资料
skydoves/compose-stability-analyzer(Gradle 插件 + 运行时 + IDE 插件)— https://github.com/skydoves/compose-stability-analyzer- skydoves,《通过掌握 Jetpack Compose 中的稳定性来优化应用性能》— https://medium.com/proandroiddev/optimize-app-performance-by-mastering-stability-in-jetpack-compose-69f40a8c785d
- skydoves,《优化应用性能的 6 条 Jetpack Compose 指南》— https://medium.com/proandroiddev/6-jetpack-compose-guidelines-to-optimize-your-app-performance-be18533721f9
- Ben Trengrove,《Jetpack Compose:调试重组》— https://medium.com/androiddevelopers/jetpack-compose-debugging-recomposition-bfcf4a6f8d37
- Ben Trengrove,《为什么应该始终在发布版本中测试 Compose 性能》— https://medium.com/androiddevelopers/why-should-you-always-test-compose-performance-in-release-4168dd0f2c71
- Compose 稳定性 — 诊断 — https://developer.android.com/develop/ui/compose/performance/stability/diagnose
- Compose 性能概览 — https://developer.android.com/develop/ui/compose/performance
如需了解使用 Layout Inspector 和 Argument Change Reasons 的调试入口,请参阅 ../../recomposition/debugging-recompositions/SKILL.md。如需了解防止下一次稳定性回归的 CI 门禁,请参阅 ../../stability/enforcing-stability-in-ci/SKILL.md。如需了解在找出问题参数后如何处理其根因,请参阅 ../../stability/diagnosing-compose-stability/SKILL.md、../../stability/stabilizing-compose-types/SKILL.md、../../recomposition/using-strong-skipping-correctly/SKILL.md 和 ../../recomposition/deferring-state-reads/SKILL.md。如需了解端到端的用户感知性能测量(帧耗时、冷启动),请参阅 ../generating-baseline-profiles/SKILL.md。