关于 debugging-recompositions
面向 Android Compose 开发者的诊断技能,借助 Layout Inspector 和运行时追踪定位发生重组的可组合项及原因。它根据参数状态指引后续排查,而不直接提供修复方案。
主要功能
- 01跟踪重组与跳过计数
- 02检查逐参数变化原因
- 03按状态确定排查方向
- 04通过发布版追踪确认行为
适用场景
- 01查找滚动或动画时重组的可组合项
- 02排查 Changed、Uncertain 或 Unknown 参数
- 03在发布版中验证重组修复
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ compose-performance-skills --skill debugging-recompositions安装后在 Claude Code 中输入/debugging-recompositions确认已加载。
详细介绍
调试重组——让不可见的内容变得可见,然后将状态映射为操作
默认情况下,重组是不可见的。每秒重新运行 60 次以重绘动画的可组合项,在源代码中的样子与一个应当在父项每次更新时都不重新运行的可组合项完全相同。三层检测工具可以让它变得可见:Layout Inspector 的重组次数和跳过次数(Android Studio,调试构建)、Layout Inspector 的参数变更原因(Hedgehog 及更高版本,按参数分类为已变更 / 未变更 / 不确定 / 静态 / 未知),以及用于发布版 / R8 / 真机测量的 compose-stability-analyzer 运行时 @TraceRecomposition。
这项技能属于诊断层。它不会修复重组,而是告诉开发者哪个可组合项正在重组,以及哪个参数导致了重组。一旦确定了参数,修复就交由相关技能处理——不稳定类型用稳定性技能,lambda 捕获用强跳过技能,错误阶段的状态读取用阶段延迟技能。
何时使用此技能
- 开发者说“这个本该跳过,却没有跳过”,或“父项每次更新时它都会重新运行”。
- 开发者想统计特定可组合项的重组次数,以确认修复是否生效。
- 开发者询问 Layout Inspector 重组原因面板中的“不确定”“未知”“静态”或“未变更”是什么意思。
- PR 声称修复了稳定性或强跳过问题;审查者需要确认发布版中是否确实不再发生重组。
- 用户提到“重组次数”“Layout Inspector”“参数变更原因”“Hedgehog”“@TraceRecomposition”或“为什么它会重组”。
何时不使用此技能
- 可组合项因不稳定类型而重组——此技能确定问题来源后,实际修复应使用
../../stability/diagnosing-compose-stability/SKILL.md和../../stability/stabilizing-compose-types/SKILL.md。 - 可组合项因 lambda 捕获或不可跳过的作用域而重组——修复工作应使用
../using-strong-skipping-correctly/SKILL.md。 - 可组合项因在错误阶段(Composition、Layout 或 Draw)读取状态而重组——修复工作应使用
../deferring-state-reads/SKILL.md。 - 团队需要设置 CI 门禁,防止未来出现回归——使用
../../stability/enforcing-stability-in-ci/SKILL.md。
前置条件
- 需要 Android Studio Hedgehog(2023.1.1) 或更高版本才能查看参数变更原因。更早版本的 Studio 可以显示重组次数,但没有按参数显示原因的面板。
- 使用调试构建以运行 Layout Inspector(Layout Inspector 需要调试构建)。只将读取到的次数视为方向性信号;调试构建会以解释执行方式运行,并启用 Live Literals,这会使重组次数虚高。
- 需要提供
androidx.lifecycle:lifecycle-runtime-compose,以便在确认问题来源是 flow 后,使用collectAsStateWithLifecycle替换普通的collectAsState。 - 如需在发布模式下确认:按
../../measurement/tracing-recompositions-at-runtime/SKILL.md配置运行时跟踪(添加compose-stability-analyzer运行时,并在Application.onCreate中调用ComposeStabilityAnalyzer.setEnabled(true))。 - 熟悉 Compose Compiler 报告格式(参见
../../stability/diagnosing-compose-stability/SKILL.md)——Layout Inspector 的分类使用相同的稳定性术语。
工作流
1. 在 Layout Inspector 中启用重组计数
在已连接的设备或模拟器上以 debug 模式运行应用。打开 Android Studio → Tools → Layout Inspector。在 Layout Inspector 工具栏中切换:
- Show recomposition counts — 在组件树中添加“Recompositions”列,并在渲染树上叠加计数徽标。
- Show recomposition skip counts — 添加相邻列,显示可组合项的跳过保护条件触发的次数(即调用了该项,但跳过了其函数体)。
这两个选项默认均为关闭。两者都开启后,树中的每个节点都会显示 <recompositions>/<skips>,这是主要的判断依据。
健康的比例是“跳过次数 ≫ 重组次数”。100/0 的可组合项(重组 100 次,跳过 0 次)就是明显的问题所在——每次父级更新都会重新执行其函数体,且没有触发任何跳过保护条件。100/95 的可组合项则运行正常——只有 5 次重组实际执行了工作。
2. 观察计数并复现问题
计数会累积。复现前,使用 Layout Inspector 中的“Reset”按钮重置计数。然后执行开发者怀疑会导致卡顿的操作——滚动、点击、动画帧更新——并观察哪些可组合项的计数在增长。
计数增长明显异常的可组合项就是可疑对象。继续之前,记下它的名称和参数列表。
3. 检查参数变化原因
在 Layout Inspector 树中点击可疑的可组合项。右侧面板现在会显示 “Recomposition reasons” 部分,其中列出了每个参数及其五种分类之一:
| 状态 | 含义 | 可能的处理方式 |
|---|---|---|
| Static | 编译时常量;不会使组合失效。 | 无需处理。此参数不是原因。 |
| Unchanged | 该值与上一次组合中的值相等(身份相同或 equals 相等)。 | 无需处理。此参数没有触发重组。 |
| Changed | 该值与上一次组合中的值不相等。 | 排查变化是有意义的还是意外的(例如,copy() 生成的内容相同,但由于某个字段是 MutableList,导致 equals 比较失败)。 |
| Uncertain | Compose 无法确定该参数自上次组合后是否发生变化;通常是因为编译器认为参数类型不稳定,因此运行时退回到身份相等比较(===),且比较结果不确定。 | 稳定该类型(添加 @Stable / @Immutable 注解,或将 List<T> 替换为 ImmutableList<T> / PersistentList<T>,以便进行结构化 equals 比较)——或者接受这次重组。 |
| Unknown | 编译器无法在静态分析时判定该类型的稳定性。运行时退回到 ===。 | 运行 ../../stability/diagnosing-compose-stability/SKILL.md,找出该类型未被分类的原因——通常是使用了未带 @StabilityInferred 的独立编译模块、接口,或来自 Java 库且未添加注解的 POJO。 |
状态名称指明了下一步该做什么;在确认状态之前,不要直接着手修复。
4. 将状态映射为操作
识别状态后,按以下决策树处理:
- 状态 = Changed。 值确实发生了变化。判断此次变化是否有意义:用户输入是否发生了变化?或者
copy()是否生成了开发者误以为相同、但结构不同的对象?常见原因:收集Flow时没有使用distinctUntilChanged(),导致发出的相等值无法通过引用相等性检查。 - 状态 = Uncertain。 身份发生了变化,内容可能相同。为类型添加注解,或使用稳定集合。参见
../../stability/stabilizing-compose-types/SKILL.md。这通常是data class copy()后未能通过实例相等性检查的明显信号。 - 状态 = Unknown。 编译器未进行分类。运行稳定性诊断。参见
../../stability/diagnosing-compose-stability/SKILL.md。最常见的原因:接口类型的参数、单独编译的 Java POJO、没有实例可用于替换的泛型参数。 - 状态 = Static。 无需操作;该参数是编译时常量。
- 状态 = Unchanged。 无需操作;该参数在重组前后相等。原因一定在其他地方(另一个参数,或父级的重启作用域无条件地重新调用)。
5. 使用 @TraceRecomposition 在发布版本中确认
Layout Inspector 的计数来自调试版本。调试版本包含 Live Literals(常量值会变成 getter),使用解释执行的 Compose 运行时,且未启用 R8。这些计数可作为方向性信号,但不能作为最终数值。要在发布模式下确认,请使用 compose-stability-analyzer 中的 @TraceRecomposition 为可疑的可组合函数添加检测,然后在启用了 R8 的发布版本中重新运行场景。完整设置请参见 ../../measurement/tracing-recompositions-at-runtime/SKILL.md,以下是两行摘要:
@TraceRecomposition(traceStates = true)
@Composable
fun SnackRow(snack: Snack, onClick: (Long) -> Unit) { /* ... */ }
Logcat 中 Recomposition 标签下会为每次重组打印一行,其中列出每个参数及其是否发生变化。发布模式下的计数才是准确结果;报告修复有效时,请引用此计数,而非调试计数。
模式
模式:不要仅凭调试计数得出结论
# WRONG
"Layout Inspector shows SnackRow at 100/0 in debug — fix shipped."
# WRONG because: debug builds run interpreted with Live Literals turning constants into
# getters that defeat compile-time folding and inflate recomposition. The same composable
# in release + R8 may run 5/95. Always confirm the final number in release with @TraceRecomposition.
# RIGHT
"Layout Inspector showed SnackRow at 100/0 in debug. After the fix, debug shows 5/95
and release-mode @TraceRecomposition confirms 0 recompositions across a 30-frame scroll."
调试计数为何不可靠,请参见 ../../measurement/testing-compose-in-release-mode/SKILL.md。
模式:先确定状态,再采取行动
# WRONG
"FilterBar is recomposing too much. I'll mark Filter @Stable and see if it helps."
# WRONG because: the developer skipped naming the Argument Change Reason. If the status was
# Unknown, the type needs classification (probably from an external module); if it was
# Uncertain, the type needs @Immutable + ImmutableList; if it was Changed, the upstream
# producer is mutating where it shouldn't. Each requires a different fix; guessing wastes a cycle.
# RIGHT
"FilterBar is recomposing per-frame. Layout Inspector reports the `filter` param as Uncertain.
The Filter type holds List<String>; replacing with ImmutableList<String> + @Immutable should
let equals() catch structural equality. Confirm with @TraceRecomposition in release."
模式:忽略 “Uncertain” 是最常见的错误
“Uncertain” 是最有助于诊断的状态。它表示:“编译器认为该类型稳定,运行时执行了比较,但身份不同。”这几乎总是由 copy() 或构建器生成结构相等但身份不同的实例造成的,同时由于某个子字段不稳定,equals 不会生效。
// WRONG — declared @Immutable but the field is mutable
@Immutable
data class Filter(val tags: List<String>, val sort: SortOrder)
// WRONG because: List<String> is unstable; @Immutable is a contract the developer broke.
// Layout Inspector reports `filter` as Uncertain on every recompose; structural equals
// does not run because a sub-field is unstable.
// RIGHT
@Immutable
data class Filter(val tags: ImmutableList<String>, val sort: SortOrder)
模式:衔接运行时跟踪,进行端到端确认
// RIGHT — annotate the suspect, run release with R8, capture logcat
@TraceRecomposition(traceStates = true)
@Composable
fun FilterBar(filter: Filter) { /* ... */ }
adb logcat -s Recomposition:D
# Expected after the fix: a single "[Recomposition #1]" on initial composition,
# then no further entries during the scroll scenario.
完整的检测说明请参见 ../../measurement/tracing-recompositions-at-runtime/SKILL.md。
模式:以跳过次数而非重组次数作为健康指标
处于 0/0 的可组合函数是死代码,或从未被执行。处于 100/0 的可组合函数存在问题。处于 100/95 的可组合函数则运行良好——父级一百次更新中,只有五次真正发生了重组。应长期跟踪跳过次数与重组次数的比率,而不是绝对计数。
强制规则
- 必须在报告重组发现时,注明 Layout Inspector 的参数变化原因状态(Changed / Unchanged / Uncertain / Static / Unknown)。「重组很多」不算发现;「每次父项发生变化时,参数
filter都是 Uncertain」才算。 - 不得将调试版 Layout Inspector 的重组次数视为真实次数。调试版使用解释执行的 Compose 和 Live Literals;该次数仅供判断趋势。请在发布版中使用
@TraceRecomposition确认。 - 必须在重现情景前重置 Layout Inspector 的计数。之前应用状态累积的计数会干扰诊断。
- 必须将每种状态映射到正确的同级技能:Uncertain / Unknown →
../../stability/diagnosing-compose-stability/SKILL.md,Changed(lambda 捕获)→../using-strong-skipping-correctly/SKILL.md,Changed(在错误阶段读取状态)→../deferring-state-reads/SKILL.md。 - 不得忽略「Uncertain」。这是最常见的危险信号,表明
@Immutable契约被可变子字段违反。 - **建议:**将 Layout Inspector 的发现串联到
../../measurement/tracing-recompositions-at-runtime/SKILL.md,以便在发布版中端到端确认。 - 建议:报告「修复有效」时,引用跳过与重组的比率(例如「100/95」或「滚动 30 帧期间重组 0 次」),而不是原始重组次数。该比率在调试版和发布版之间保持一致,原始次数则不会。
默认情况下,重组不可见;Layout Inspector、Argument Change Reasons 和 @TraceRecomposition 三者构成诊断工具链。可跳过性仍是诊断手段,而不是关键绩效指标——目标是「用户感知的高频路径上没有意外重组」,而不是「到处都没有重组」。
验证
- 已开启 Layout Inspector 中的「Show recomposition counts」和「Show recomposition skip counts」。
- 已在重现情景前重置计数。
- 已根据 Recompositions 列相较于跳过次数的不成比例增长,确定可疑的可组合项。
- 已检查可疑项的 Argument Change Reasons 面板;报告中注明了每个参数的状态(Changed / Unchanged / Uncertain / Static / Unknown)。
- 已将状态映射到正确的同级修复技能;已应用修复;如适用,已更新基线(
../../stability/enforcing-stability-in-ci/SKILL.md)。 - 在发布版 + R8 构建中使用
@TraceRecomposition确认,修复后的重组次数符合开发者的预期。调试版数字仅供判断趋势;此处的数字才是最终结果。
参考资料
- Ben Trengrove,《Jetpack Compose:调试重组》—— https://medium.com/androiddevelopers/jetpack-compose-debugging-recomposition-bfcf4a6f8d37
- Layout Inspector — https://developer.android.com/studio/debug/layout-inspector
- Compose 性能概览 — https://developer.android.com/develop/ui/compose/performance
- Compose 稳定性 — 诊断 — https://developer.android.com/develop/ui/compose/performance/stability/diagnose
- Ben Trengrove,《为什么应该始终在 release 模式下测试 Compose 性能》—— https://medium.com/androiddevelopers/why-should-you-always-test-compose-performance-in-release-4168dd0f2c71
- Chris Banes,《可组合项指标》—— https://chrisbanes.me/posts/composable-metrics/
- skydoves/compose-stability-analyzer(运行时
@TraceRecomposition)—— https://github.com/skydoves/compose-stability-analyzer - skydoves,《6 条 Jetpack Compose 指南》—— https://medium.com/proandroiddev/6-jetpack-compose-guidelines-to-optimize-your-app-performance-be18533721f9
关于状态为 Uncertain 或 Unknown 时的上游稳定性修复,请参阅 ../../stability/diagnosing-compose-stability/SKILL.md 和 ../../stability/stabilizing-compose-types/SKILL.md。关于 Changed 的 lambda 捕获原因,请参阅 ../using-strong-skipping-correctly/SKILL.md。关于在 release + R8 构建中确认修复,请参阅 ../../measurement/tracing-recompositions-at-runtime/SKILL.md 和 ../../measurement/testing-compose-in-release-mode/SKILL.md。关于防止未来回归,请参阅 ../../stability/enforcing-stability-in-ci/SKILL.md。