重组性能分析工具

大约 11 分钟

重组性能分析工具

本页说明如何使用 Kuikly Compose 内建的 RecompositionProfiler 工具,在调试阶段定位重组性能问题。

主要能力:

  • 自动追踪所有 Composable 的重组次数与耗时(无需修改业务代码)
  • 精确显示触发重组的 State 对象及其值变化(prev → now
  • 检测参数级变更(哪个参数导致了本次重组)
  • 悬浮热点面板(Overlay),实时可视化重组热点
  • 自定义过滤:按名称或包名前缀排除业务基础组件,聚焦核心业务逻辑
  • 自动写文件(JSON 报告 + 逐帧数据),供 AI 离线分析
  • 多平台一致:iOS、Android、HarmonyOS 行为相同

快速开始

1. 启动 Profiler

// 配置(可选,不调用则使用默认配置)
RecompositionProfiler.configure {
    sampleRate = 1.0f          // 采样率,1.0 = 全量采集
    hotspotThreshold = 10      // 热点阈值
    enableLog = true           // 默认 true,开启日志输出
    enableFile = true          // 默认 true,开启文件写入,供 AI 离线分析
    enableOverlay = true       // 启用悬浮热点面板(默认 false)
}

// 启动
RecompositionProfiler.start()

默认行为start() 后自动开启日志输出(enableLog = true)和文件写入(enableFile = true),文件写入用于 AI 离线分析重组问题,无需手动配置。

2. 获取报告

// 获取结构化报告(Profiler 运行中或 stop 后均可调用)
// saveToFile=true(默认)会同时将报告写入 profiler_report.json
val report = RecompositionProfiler.getReport()

// 输出 JSON
println(report.toJson())

3. 停止 Profiler

RecompositionProfiler.stop()
// stop 后自动写 profiler_report.json,仍可调用 getReport() 获取数据

4. 重置数据

RecompositionProfiler.reset()

清空当前会话内所有已采集的重组数据(帧事件、组件统计、State 变更记录),计数从零重新开始。适合在切换测试场景时使用,Profiler 保持运行状态不中断。


自定义过滤

业务项目中通常有大量基础组件(如通用按钮、Loading 组件等)频繁重组,这些噪声会干扰对核心业务逻辑的分析。通过自定义过滤,可以将指定组件从面板和日志中排除,聚焦真正需要关注的重组。

按名称精确排除

// 按短函数名排除(同名函数在不同文件中会被同时排除)
RecompositionProfiler.excludeByName("MyBaseButton", "CommonLoading")

// 也支持 List 传参
RecompositionProfiler.excludeByName(listOf("MyBaseButton", "CommonLoading"))

// 按名称+源码位置精确排除(只排除指定文件中的同名函数)
RecompositionProfiler.excludeByName(listOf("invoke"), sourceLocation = "FeedsDoubleColumnCard.kt:47")

按前缀批量排除

// 按包名前缀排除(匹配全限定名)
RecompositionProfiler.excludeByPrefix("com.myapp.foundation.", "com.myapp.common.")

// 按函数名前缀排除(匹配短函数名)
RecompositionProfiler.excludeByPrefix("<get-", "remember")

前缀匹配同时检查包名全限定名短函数名

  • "com.myapp.foundation." 会匹配 info 中以该包名开头的所有 Composable
  • "<get-" 会匹配短函数名以 <get- 开头的 Composable(如 Kotlin 编译器生成的属性 getter <get-colorScheme>

清空过滤规则

// 清空所有业务自定义过滤规则(内置框架过滤不受影响)
RecompositionProfiler.clearCustomFilters()

配置时机

过滤规则可在 start() 前后任意时刻配置,立即生效:

// 方式一:start 前配置,从第一帧起就过滤
RecompositionProfiler.excludeByName("MyWidget")
RecompositionProfiler.start()

// 方式二:运行中动态添加
RecompositionProfiler.start()
// ... 观察一段时间后 ...
RecompositionProfiler.excludeByName("NoisyComponent")

规则持久性

  • stop/start 跨 session:过滤规则在 stop() 后保留,下次 start() 仍生效
  • 幂等:重复添加同一名称无副作用(Set 语义)
  • 空字符串:会被自动忽略

输出格式

日志格式

日志 Tag 为 RCProfiler,每行单独输出,可用此 Tag 过滤所有重组日志:

# Android
adb logcat -s "RCProfiler"

# iOS 控制台(console.log)
grep "RCProfiler" logs/kuikly_console.log

# HarmonyOS
grep "RCProfiler" logs/kuikly_ohos.log

每帧只要发生重组就输出一个 Frame 块(无重组的帧不输出),每行独立带 Tag:

[RCProfiler] Frame #42 START (ts=1774431764564ms)
[RCProfiler]   RECOMPOSED: CounterSection @RecompositionProfilerDemoPage.kt:221 (1ms) [parent=<unknown>] params=[no params change] triggers=[State(prev=1, now=2), readers: CounterSection]
[RCProfiler]   RECOMPOSED: LambdaChild @RecompositionProfilerDemoPage.kt:331 (0ms) [parent=ParentChildDemo] params changed: [#1] (1/2) triggers=[]
[RCProfiler] Frame #42 END (duration=5ms, recomposed=2)

字段说明:

字段说明
Frame #N帧序号,从 1 开始累计递增(包含无重组的帧),相邻输出的帧号可能不连续,属正常现象
@File.kt:LineComposable 函数的源码声明位置(编译器限制,不是调用位置)
(Xms)本次重组耗时
params=[no params change]参数未变化,重组由 State 变化触发
params changed: [#0, #2]参数编号 #0, #1, #2... 对应 Compose 编译器 $dirty bitmask 的 slot 位置。如果参数总数比函数签名多 1,则 #0 是 receiver(this),见下方参数编号规则
triggers=[State(prev=1, now=2)]触发此次重组的 State 及值变化
parent父 Composable 名称。如果最近的父是匿名 lambda(<anonymous>),会自动向上跳过,找到最近的有名父组件。例如 invoke 的父原本是 <anonymous>,日志中显示为更上层的 FeedsDoubleColumnCard
scope=NRecomposeScope 的 hashCode,详见下方 Scope 字段说明
scope=none无独立 RecomposeScope,详见下方说明

Scope 字段说明

scope 标识触发重组的 RecomposeScope(Compose Runtime 的最小重组单元)。

scope=N(有值)

N 是 RecomposeScopehashCode(),可与逐帧日志 [scope=N] 和报告中的 scope 分布直接对应搜索。

  • 同一函数相同 scope=N → 同一个槽位实例被多次重组。关注频率是否异常 — 如果单个槽位重组次数远超其他槽位,通常是性能问题根源
  • 不同函数相同 scope=N → 它们在同一个 scope 的执行链路内,由同一个 State 变化驱动。最外层的 Composable 拥有该 scope,内层的是被级联重新执行的

scope=none

该 Composable 没有独立 RecomposeScope,可能的原因:

  • 首次组合(Initial Composition):页面首次加载,没有 scope 被 invalidate
  • 级联重组:因父 Composable 重组而重新执行,自身没有读取 State
  • LazyColumn sub-composition:LazyColumn item 在首次出现或参数驱动重组时,CompositionObserver 无法为 sub-composition 建立 scope 映射

报告中的 Scope 分布

汇总报告中每个 Composable 下方会附加 scope 分布信息:

  FeedsImageTextCard @FeedsImageTextCard.kt:30: 120x (avg=1.2ms)
    → scopes: {212877427: 45x, 339201: 30x, 1058823: 20x}, no-scope: 15
  • {212877427: 45x, ...} — 每个 scope key 对应的重组次数,按次数降序。可用 scope key 在逐帧日志中搜索 scope=212877427 定位具体帧
  • no-scope: 15 — 无独立 scope 的重组次数

参数编号规则

Compose 编译器的 $dirty bitmask 中,参数按 slot 位置编号 #0, #1, #2...

函数类型#0#1#2参数总数 vs 签名参数数
成员函数 class Foo { @Composable fun bar(a, b) }this(Foo)ab总数 = 签名参数 + 1
扩展函数 @Composable fun Foo.ext(a)this(Foo)a总数 = 签名参数 + 1
顶层函数 @Composable fun topLevel(a, b)ab总数 = 签名参数数

如何判断 #0 是否是 receiver:如果日志显示的参数总数比函数签名声明的参数多 1,则 #0 是 receiver(this),其余参数从 #1 开始对应签名参数。例如函数签名有 2 个参数但日志显示 (1/3),说明 #0 是 this,#1 和 #2 对应签名中的 2 个参数。

  • #0(receiver) 变化通常意味着创建了新实例(默认 equals() 是引用比较)

Overlay 热点面板

启用 enableOverlay = true 后,页面右下角出现悬浮圆形按钮,可拖动位置:

Overlay FABOverlay 展开面板
悬浮 FAB展开热点面板
  • 正常态:显示当前会话累计重组次数,绿色(无重组)→ 橙色 → 红色(高频重组)
  • 暂停态:显示 ||,数据更新暂停但 Overlay 仍可见
  • 点击展开:居中面板展示热点列表

热点面板功能

按钮功能
暂停 / 继续暂停或恢复 Overlay 数据更新(不影响底层采集,也不隐藏面板)
重置清空所有计数,等同于 RecompositionProfiler.reset()。过滤规则保留不受影响
报告将完整 JSON 报告输出到控制台日志
清空过滤清空所有业务自定义过滤规则,等同于 RecompositionProfiler.clearCustomFilters()

热点列表说明

  • 函数名+源码位置聚合,不同文件中的同名函数分开统计(如多个 invoke
  • 包含首次组合和重组,按总次数降序排列
  • 每条显示:组件名(左)、累计次数(右)、源码声明位置(第二行灰色小字)
  • 最多展示 overlayTopCount 条(默认 50),超出部分可滚动查看
  • 点击「过滤」按钮可精确排除该条目(按名称+源码位置,不影响其他文件的同名函数)

为什么不按实例区分:Compose Runtime 的 traceEventStart key 是函数维度(非调用点),不同驱动方式(State 失效 vs 参数驱动)下实例 key 的可用性不一致,强行区分会导致部分组件有序号、部分没有,行为不统一。实例级细节可通过日志的 RECOMPOSED 输出(含 parent 信息)查看。


配置项说明

配置项类型默认值说明
sampleRateFloat1.0采样率(0.0~1.0)。0.5 表示约 50% 的帧被记录,可降低性能开销
hotspotThresholdInt10热点判定阈值:Report 中 isHotspot = true 的判定条件
maxEventBufferSizeInt100000事件缓冲区最大容量,超出时丢弃最旧事件
includeFrameworkComposablesBooleanfalse是否包含框架内部 Composable(Row/Column 等)。默认只监控业务代码
enableBuiltinFiltersBooleantrue是否启用内置框架过滤。关闭后框架 Composable 也会被记录
customFiltersList[]静态自定义过滤器列表(实现 ComposableFilter 接口)。与 excludeByName/excludeByPrefix 共存互不覆盖
enableLogBooleantrue是否开启日志输出。仅在 start/stop 期间有效
enableFileBooleantrue是否开启文件写入,供 AI 离线分析。仅在 start/stop 期间有效
enableOverlayBooleanfalse启用悬浮热点面板。需在 start() 前设置
overlayTopCountInt50热点面板展示的最大条目数(1~100)

注意事项

  • 性能:Profiler OFF 时零开销(编译器注入的 trace 调用被短路)。建议只在开发/测试包中启用,不要在生产包中 start()
  • 数据范围RecompositionProfiler 是全局单例,start() 后所有页面的重组都会被采集,多页面跳转时数据累积在一起。如需按页面分析,建议在进入目标页面时 reset()
  • Overlay 开关enableOverlay 需要在 start() 之前通过 configure { } 设置。
  • 采样率:高频重组场景(如 60fps 动画)可设置 sampleRate = 0.3 降低日志量。
  • 文件位置:写入 App 沙盒目录(iOS/Android 写 Caches,HarmonyOS 写 files),系统磁盘紧张时 Caches 可能被清理;分析完建议及时备份。

AI 辅助分析(Recomposition Analyzer Skill)

前置条件

  1. 确保 enableFile = true(默认已开启),Profiler 才会输出文件供 AI 分析:
RecompositionProfiler.configure {
    enableFile = true     // 确保开启文件输出
    enableLog = true      // 可选,开启 KLog 实时输出
}
  1. 使用 debug 包运行 App(release 包通常不包含 Profiler)
  2. 采集数据:打开目标页面 → 启动 Profiler → 执行要分析的操作(滚动、点击等)→ 停止 Profiler → 点击「获取报告」

完成以上步骤后,设备上已生成 profiler_report.jsonprofiler_frames.jsonl,可以让 AI 介入分析。

使用方式

在支持 Skills 的 AI 编码工具(如 CodeBuddy Code、Claude Code 等)中,指定平台和包名:

用 kuikly-recomposition-analyzer 分析 android 模拟器上 com.tencent.news.core 的重组问题

或指定已拉取到本地的文件路径:

用 kuikly-recomposition-analyzer 分析这两个文件:
- report: /tmp/profiler_report.json
- frames: /tmp/profiler_frames.jsonl

分析流程

Skill 采用三阶段漏斗分析,逐层收窄范围:

  1. Report 筛查 — 读取 report.json,按阈值过滤出嫌疑组件(scope 高频触发、参数高频变化、State 广播等)
  2. Frames 深挖 — 读取 frames.jsonl,确认帧级问题(帧卡顿、级联重组),结合 touch/scroll 上下文事件判断重组是否正常
  3. 源码确认 — 对嫌疑组件定位业务源码,分析根因并给出具体优化建议

输出产物

  • 对话摘要:TOP 3 问题的高层概览
  • Markdown 报告文件recomp-analysis-YYYYMMDD-HHmm.md):包含数据概览、正常重组清单、问题诊断(按严重度降序)、帧级卡顿分析、优化建议

自定义阈值

分析时可覆盖默认阈值:

重组分析,scopeCountThreshold=10,durationThreshold=8
配置项默认值说明
scopeCountThreshold5某 scope 重组次数超过此值标记为嫌疑
durationThreshold5ms单次重组耗时超过此值标记为嫌疑
singleRecompDurationThreshold10ms单次峰值耗时超过此值必须输出到报告
frameDurationThreshold16ms单帧耗时超过此值标记为帧卡顿
paramChangeRateThreshold0.9参数变化率超过此值标记为参数不稳定
stateReadersThreshold3State 的 readers 数超过此值标记为广播

聚焦特定页面

如果只想分析某个页面的重组:

  1. 在 Profiler Overlay 面板点「重置」
  2. 进入目标页面执行一遍操作
  3. 停止 Profiler,获取报告
  4. 告诉 AI 分析