第3章 使用Compose Compiler Metrics分析组合耗时

3.1 原理概述

Jetpack Compose 编译器在编译阶段可以输出 Compose Compiler Metrics,这是一种 静态分析报告,记录了每个可组合函数预期的 组合次数、重组触发频率 以及 单次组合的估算耗时。与运行时工具(如 Layout Inspector)不同,Metrics 不需要运行 App,即可提前暴露潜在的性能瓶颈,帮助开发者在 编译时 发现高耗时节点。

适用场景:大型列表项、复杂 UI 树、频繁重组的父容器、使用了大量 State 或 derivedStateOf 的组件。

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 目录下。

注意: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)。
实战建议:将 Metrics 报告与 Compose Stabilizer 插件(或手动 @Stable 注解)结合,先修复 unstable 参数,再优化 cost 高的节点。

3.5 分析流程图

Compose Compiler Metrics 分析流程 1. 开启编译器参数 2. ./gradlew assembleDebug 3. build/compose_metrics/ compose-reports.txt 4. 解读字段 • frequency (stable/unknown) • cost(耗时权重) 5. 标记高耗时节点 cost > 20 或 unknown 组合次数 > 1 6. 执行优化 @Stable / 提取状态 减少 if/for 嵌套 ⚠ 常见错误 & 技巧 • 漏开 metricsDestination 或 reportsDestination → 没有输出文件。 • 误把 cost 当作时间;它只是相对权重,需结合重组频率判断。 • .json 格式适合脚本分析;.txt 适合人工阅读。

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 成为门禁标准之一。

本章核心收益:学会通过 Compiler Metrics 在编译阶段快速扫描组合性能隐患,做到“未经运行,已知瓶颈”,显著降低运行时调试成本。