mcpField
T
tracing-recompositions-at-runtime

tracing-recompositions-at-runtime

github.com/skydoves开发520126 次安装19 周前更新
代码仓库

关于 tracing-recompositions-at-runtime

此技能介绍如何用 `skydoves/compose-stability-analyzer` 的编译期插桩诊断 Jetpack Compose 的过度重组。Android 开发者可在 logcat 查看参数与状态变化,也可使用 IDE 热图,并在接近发布的构建中追踪。

主要功能

  1. 01编译期 `@TraceRecomposition` 插桩
  2. 02参数和状态变化差异
  3. 03基于 logcat 的重组追踪
  4. 04可选的 IDE 重组热图
  5. 05通过运行时开关控制输出

适用场景

  1. 01调查调试版与发布版重组次数差异
  2. 02在接近发布的构建中验证稳定性修复
  3. 03查找重组背后的参数或状态变化
  4. 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 中的 stabilityCheck CI 门禁结合使用。运行时跟踪用于诊断;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),以便在下一次回归发布前将其捕获。

参考资料

如需了解使用 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。

评论

支持 Markdown