第3章 使用Compose Compiler Metrics分析组合耗时
3.1 原理概述
Jetpack Compose 编译器在编译阶段可以输出 Compose Compiler Metrics,这是一种 静态分析报告,记录了每个可组合函数预期的 组合次数、重组触发频率 以及 单次组合的估算耗时。与运行时工具(如 Layout Inspector)不同,Metrics 不需要运行 App,即可提前暴露潜在的性能瓶颈,帮助开发者在 编译时 发现高耗时节点。
3.2 开启 compileOption metrics
在项目的 build.gradle.kts(或 build.gradle)模块级别中,向 composeOptions 添加 kotlinCompilerExtensionVersion 并启用 metrics 输出:
android {
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4" // 与你使用的 Compose BOM 版本匹配
}
kotlinOptions {
freeCompilerArgs += listOf(
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
freeCompilerArgs += listOf(
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
}
}
如果使用 应用级 配置(非模块级),需在 android.kotlinOptions 中重复上述参数。同步后执行:
./gradlew assembleDebug
生成的 .txt 和 .json 文件将出现在 build/compose_metrics 目录下。
assemble 或 compileDebugKotlin 等编译任务时生成,./gradlew tasks 不会自动触发;建议使用 assembleDebug 确保全部源文件编译。
3.3 读懂生成的组合顺序与频率
典型的 Metrics 输出(compose-reports.txt)包含以下关键节:
| 字段 | 含义 | 示例值 |
|---|---|---|
@Composable calls | 该可组合函数内调用的其他 @Composable 函数列表 | LazyColumn, Text, Button |
composite count | 静态预估的单次组合中该节点被处理的次数(受 if、for、lambda 影响) | 1 或 3 |
composable frequency | 编译器推断的预期重组频率:stable(稳定)、known(已知不稳定)、unknown(全重组) | stable |
composition cost | 基于函数体复杂度估算的“耗时”权重(相对值) | 10(高) |
解析示例:
@Composable fun MyScreen(
username: String,
items: List-
)
@Composable calls:
Text(username) // stable, frequency=stable, cost=2
LazyColumn(items) // stable, frequency=known, cost=50
@Composable calls:
ItemRow(item, onRemove) // unstable (lambda in param), frequency=unknown, cost=30
可快速发现:ItemRow 因为接收 lambda 参数导致频率为 unknown(每次列表变化都重组合),且 cost=30,是典型高耗时节点。
3.4 定位高耗时节点 🎯
关注以下 三类危险信号:
- frequency = unknown:该函数或它的参数被判定为不稳定,导致父容器每次重组时都会刷新整个子树。
- cost 值远高于同级节点:比如同一 LazyColumn 中某项 cost=50,其余为 2~5,说明该项内部布局复杂或存在大量不变性判断。
- 嵌套的 composite count > 1:循环或条件分支导致同一个 @Composable 在同一组合内执行多次(如
for循环内的 Text)。
@Stable 注解)结合,先修复 unstable 参数,再优化 cost 高的节点。
3.5 分析流程图
3.6 实战案例:定位 LazyColumn 列表项高耗时
@Composable
fun ContactList(contacts: List, onDelete: (Contact) -> Unit) {
LazyColumn {
items(contacts) { contact ->
ContactItem(contact, onDelete) // 重点关注项
}
}
}
@Composable
fun ContactItem(contact: Contact, onDelete: (Contact) -> Unit) {
Row {
Text(contact.name)
IconButton(onClick = { onDelete(contact) }) { ... }
}
}
Metrics 输出片段:
@Composable fun ContactItem(
contact: Contact, // stable (data class with primitive)
onDelete: (Contact) -> Unit // unstable (function type)
)
recompose frequency: unknown // 因为 onDelete 参数是 lambda
composition cost: 45 // 过高(正常应在5~15)
优化方案:将 onDelete 改为稳定引用(使用 remember 包裹)或将 ContactItem 拆分为 stable 状态:
@Composable
fun ContactList(contacts: List, onDelete: (Contact) -> Unit) {
val stableOnDelete = remember { onDelete }
LazyColumn {
items(contacts) { contact ->
ContactItem(contact, stableOnDelete) // 现在 stableOnDelete 稳定
}
}
}
重新编译后 Metrics 显示 recompose frequency: stable,cost 降至 12。
3.7 常见错误与性能稳定性优化要点
| 错误 / 问题 | 表现 | 解决方案 |
|---|---|---|
| 未生成 metrics 文件 | build/compose_metrics 目录为空 | 检查 kotlinCompilerExtensionVersion ≥ 1.5.0;确保执行了 assembleDebug 而非仅 compileKotlin |
所有组合函数 frequency 均为 unknown | 整个页面每次都完全重组 | 参数中存在大量 lambda 或可变集合,使用 @Stable 或 @Immutable 注解 |
| cost 极高但找不到具体原因 | 单节点 cost > 100 | 检查是否存在 with(LocalDensity.current)、Modifier.drawBehind 等耗计算操作,提取到 remember 中 |
| 组合次数 > 1 的非预期调用 | 同一 @Composable 在同一组合中执行多次 | 减少循环内部的 if(condition) { Composable } 分支;改用 AnimatedVisibility 或 when 的稳定形式 |
3.8 扩展:自动化分析流水线
将 Metrics 报告集成到 CI 中,用于阻止高耗时代码合入:
# 解析 compose-reports.txt 并提取 cost > 30 或 frequency=unknown 的节点
grep -E "composition cost: [3-9][0-9]|frequency: unknown" build/compose_metrics/compose-reports.txt > high_cost_nodes.txt
if [ -s high_cost_nodes.txt ]; then
echo "❌ 发现高耗时组合函数,请优化后再合入"
cat high_cost_nodes.txt
exit 1
fi
此脚本可放入 .gitlab-ci.yml 或 ci.yml 中,让 Metrics 成为门禁标准之一。