4. 数据采集工具链:Systrace/Perfetto、Profiler、自定义埋点、time 命令

4.1 现象识别与工具选型判断

当 Compose 启动出现疑似 ANR 时,现象层面通常表现为:点击图标后界面长时间不响应、启动后首帧渲染超过 5 秒、或系统弹出“应用无响应”对话框。此时,工程师面临的第一道决策是:应该用哪一套工具采集数据?

不同工具对应的判定规则如下:

现象特征 推荐工具 采集目标
主线程长时间阻塞(> 2s) Systrace / Perfetto 主线程调度状态、Binder 调用、锁竞争
Composition / Layout 阶段耗时异常 Profiler + 自定义埋点 Compose 各阶段(Composition → Layout → Draw)耗时分解
启动命令行总耗时 > 5 秒 time 命令 进程启动 → Activity.onCreate → 首帧渲染的各段 wall time
无明显阻塞但首帧延迟 自定义埋点 + Systrace 自定义标签 业务代码执行路径、状态初始化、数据加载
工具选型决策规则: 先使用 Systrace/Perfetto 采集全局 trace,观察主线程是否有超过 500ms 的连续非 Runnable 状态(如 Sleeping、Uninterruptible Sleep)。若存在,则优先排查锁或 I/O;若主线程始终 Running 但耗时集中,则切入 Profiler + 自定义埋点做细粒度分解。

4.2 Systrace / Perfetto:全局追踪与线程调度

Systrace(Android 10 及以下)与 Perfetto(Android 11+)是系统级追踪工具,可捕获内核调度、Binder 通信、SurfaceFlinger 帧边界以及应用自定义 trace 标签。

4.2.1 采集命令与参数选择

# 使用 Perfetto 采集 Compose 启动过程(推荐)
adb shell perfetto \
  -o /data/misc/perfetto-traces/compose_start.trace \
  -t 15s \
  sched freq idle binder_driver gfx \
  --app com.example.yourapp

# 使用 Systrace(Android 10 及以下)
python systrace.py \
  -t 10 \
  -a com.example.yourapp \
  -o compose_start.html \
  sched freq idle gfx view binder_driver

关键标签说明:

  • sched:线程调度状态切换,发现主线程是否被内核抢占
  • freq:CPU 频率,判断是否因降频导致执行缓慢
  • binder_driver:Binder 事务统计,排查 IPC 阻塞
  • gfx:帧边界与渲染管线

4.2.2 故障树:主线程“非 Running”状态排查

在 Perfetto UI 中选中主线程,观察其调度状态切片。若出现连续超过 1 秒的非 Running 时段,按以下故障树定位:

  1. 状态为 Sleeping → 主线程主动调用了 Object.wait() 或 Thread.sleep()
    • 展开主线程栈,查找 java.lang.Object.wait 附近的调用栈
    • 常见场景:启动时等待 CountDownLatch、Future.get() 或锁竞争
  2. 状态为 Uninterruptible Sleep (D) → 主线程发生内核级阻塞
    • 通常与 I/O 操作(文件读写、mmap 缺页异常)相关
    • 在 Perfetto 中查找对应的“frequent_mmap”、“blocked”事件
    • 排查 SharedPreferences 写入、文件加载等是否在主线程进行
  3. 状态为 Running 但无 Compose 相关工作出现 → 业务初始化挤占渲染
    • 检查 Activity.onCreate 到第一帧 Choreographer 回调之间的时长
    • 观察是否有长时间执行的 Firebase/App Startup 或 ContentProvider 初始化

4.3 Profiler:CPU 方法与内存分配分解

Android Studio Profiler 提供基于采样的 CPU 火焰图和内存分配追踪,适合定位 Compose 内部方法(如 Measure、Layout、Recompose)的耗时分布。

4.3.1 Profiler 采集步骤

  1. 连接设备,运行 App
  2. 打开 Profiler → 选 CPU 标签
  3. 点击“Record”,选择 “Trace Java Methods” 或 “Sample Java Methods”
  4. 执行冷启动操作(杀死进程后重新打开)
  5. 停止录制,等待火焰图生成
采集陷阱: “Trace Java Methods” 模式会对每个方法调用产生额外开销(约 10–20 微秒/次),在 Compose 这种高频率调用场景下可能导致启动总时间延长 30% 以上。建议优先使用“Sample Java Methods”或 Perfetto 的自定义标签。

4.3.2 火焰图判读规则

火焰图特征 可能原因 修复方向
Composer.composeContent 占据宽块 Composition 阶段执行了大量重组 使用 remember / derivedStateOf 减少重组范围
LayoutNode.measure 反复出现 布局约束导致多次重新测量 使用固定约束或 IntrinsicSize 减少测量次数
Canvas.draw 或 DrawScope 相关方法宽 自定义绘制或复杂图形 考虑使用 VectorDrawable 或调低绘制复杂度
存在 StrictMode 警告对应的方法 主线程执行了磁盘 I/O 或网络请求 迁移到协程或 WorkManager

4.4 自定义埋点:Compose 阶段的精确度量

Systrace/Perfetto 的系统标签粒度不足以覆盖 Compose 内部的 三阶段(Composition → Layout → Draw)分解。自定义埋点通过在关键入口插入 trace 标记,实现微秒级定位。

4.4.1 埋点实现标准

// 定义 Trace 工具类
object ComposeTrace {
    fun beginSection(sectionName: String) {
        if (BuildConfig.DEBUG) {
            androidx.tracing.Trace.beginSection("Compose:" + sectionName)
        }
    }

    fun endSection() {
        if (BuildConfig.DEBUG) {
            androidx.tracing.Trace.endSection()
        }
    }
}

// 在 Composable 中埋点
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ComposeTrace.beginSection("Activity.onCreate")
        // ... 初始化逻辑 ...
        ComposeTrace.endSection()

        setContent {
            ComposeTrace.beginSection("setContent")
            ComposeTrace.endSection()

            ComposeTrace.beginSection("Composition:HomeScreen")
            HomeScreen()
            ComposeTrace.endSection()
        }
    }
}

@Composable
fun HomeScreen() {
    ComposeTrace.beginSection("HomeScreen.Composition")
    // 使用 LaunchedEffect 等确保只执行一次测量
    LaunchedEffect(Unit) {
        ComposeTrace.beginSection("HomeScreen.LaunchedEffect:initData")
        // 数据加载逻辑
        ComposeTrace.endSection()
    }
    ComposeTrace.endSection()
}

4.4.2 埋点数据分析方法

在 Perfetto UI 中,搜索“Compose:”前缀即可看到所有自定义标签。计算 Compose 三阶段耗时:

阶段标签 正常耗时参考 ANR 阈值
Compose:Activity.onCreate < 200ms > 1000ms
Compose:setContent < 50ms > 300ms
Compose:HomeScreen.Composition < 100ms > 500ms
Compose:HomeScreen.LaunchedEffect:initData < 50ms > 200ms
最佳实践: 埋点范围应覆盖 Compose 的 setContent 内所有顶层 Composable,以及所有会触发 LaunchedEffect / SnapshotEffect 的重组路径。生产环境通过 BuildConfig.DEBUG 开关确保无性能损耗。

4.5 time 命令:启动分段计时

time 命令提供进程启动的三大指标:real(挂墙时间)、user(用户态 CPU 时间)、sys(内核态 CPU 时间)。配合 am start -W 可获取 ActivityManager 汇报的精确分解。

4.5.1 标准采集命令

# 冷启动总时间(包括进程创建、Activity.onCreate、首帧渲染)
adb shell am start -W -n com.example.yourapp/.MainActivity

# 输出示例:
# Starting: Intent { ... }
# Status: ok
# LaunchState: COLD
# Activity: com.example.yourapp/.MainActivity
# TotalTime: 4870
# WaitTime: 4982
# Complete

关键指标含义:

  • TotalTime:从 Activity.startActivity 到首帧显示的时间(单位 ms),不包含系统启动 ActivityManagerService 的时间
  • WaitTime:TotalTime + AMS 处理延迟,通常比 TotalTime 长 50–200ms
  • 当 TotalTime > 5000ms 或 WaitTime > 8000ms 时,ANR 风险极高

4.5.2 time 命令与 Systrace 配合使用

# 在 adb shell 中用 time 监听进程启动
adb shell "time (am start -n com.example.yourapp/.MainActivity &)"

# 输出:
# real    0m5.123s
# user    0m2.340s
# sys     0m0.890s

数据交叉验证:

  • 若 real 远大于 user + sys,说明进程大量时间处于阻塞状态(I/O、锁、内核等待)
  • 若 user + sys 接近 real,则问题主要在 CPU 密集型计算
  • 将 real 与 WaitTime 对比,判断是系统调度延迟还是应用自身耗时

4.6 工具链选择决策流程

综合以上工具特性,形成以下数据采集决策树。该流程优先使用低开销的全局工具,再逐步收敛到高精度局部工具。

数据采集工具链决策流程 现象:启动延迟/ANR 步骤1: Perfetto 全局 trace → 主线程调度状态 主线程存在 >1s 阻塞? (Sleep/D-State) 是 排查锁竞争/Binder/IPC 或 I/O 操作 否 步骤2: Profiler / 自定义埋点 数据整理 → 原因定位 推荐使用 time 命令辅助验证 real / user / sys 交叉对比

4.7 工具链组合使用典型案例

案例背景: 一款 Compose 电商首页,冷启动平均耗时 6.2s,用户反馈 ANR 率为 3.5%。

数据采集过程:

  1. time 命令: am start -W TotalTime=6200,确认冷启动超过 ANR 阈值
  2. Perfetto 全局 trace: 主线程在 Activity.onCreate 阶段出现 800ms Uninterruptible Sleep,对应 SharedPreferences 文件写入
  3. 自定义埋点: 在 setContent 中插入标签,发现 HomeScreen.Composition 耗时 1.2s,其中内部列表的 LazyColumn 因未使用 key 参数导致大量重组
  4. Profiler 火焰图: 确认 LazyColumn.measure 和 HomeViewModel.loadData 各占 40% 主线程时间
案例启示: 单一工具无法覆盖 ANR 的所有根因。Perfetto 发现了 I/O 阻塞,但无法解释为什么 I/O 之后仍有大量 CPU 时间;Profiler 揭示了重组问题,但无法显示内核调度状态。只有工具链组合才能完整定位到:SP 写入 + 无 key 的 LazyColumn + ViewModel 同步数据加载 三重因素。