4. 相机启动时间测量

做启动优化,第一件事就是把“感觉慢”变成“具体数字”。我见过太多开发者一上来就改代码,改完也不知道快了多少——这其实是在盲调。今天我们来聊聊怎么用 adb、TraceView、Systrace 和 Perfetto 这四把尺子,把 CameraX 启动的每一毫秒都量清楚。

我个人习惯:先跑一遍全流程,记个总耗时。然后挨个工具深入,最后回到 adb 确认线上效果。顺序很重要,别一上来就上 Perfetto,信息太多反而找不到重点。
相机启动时间测量工具选择流程 相机启动时间测量工具决策流程 测量目标:定位启动耗时瓶颈 需要什么粒度? 粗略总耗时 adb logcat + 打桩 方法级耗时 (CPU) TraceView (Debug.startMethodTracing) 系统级 + 线程调度 Systrace → Perfetto 产出:精确到毫秒的启动流水 注意:不同工具开销不同。adb 打桩基本无开销;TraceView 会让方法变慢 2-5 倍; Systrace/Perfetto 开销极低(<5%),适合线上预研。建议先用 Perfetto 扫全局,再用 TraceView 抠细节。

4.1 最快的一刀:adb logcat + 时间打桩

不管用什么高级工具,我都建议先跑一遍 adb 打桩。原因很简单——快、准、不干扰。你只需要在关键节点加上 Log.d,然后通过 adb logcat 抓取时间差。

// 在 CameraX 启动关键路径上打桩
val startTime = System.currentTimeMillis()
Log.d("CameraStartup", "1: onStart at $startTime")

// .... 中间过程 ...

val readyTime = System.currentTimeMillis()
Log.d("CameraStartup", "9: preview ready at $readyTime , delta = ${readyTime - startTime}")

然后过滤输出:

adb logcat -s CameraStartup -v time

输出类似这样:

时间戳标签信息
05-12 14:23:45.123CameraStartup1: onStart at 1715506425123
05-12 14:23:45.456CameraStartup9: preview ready at 1715506425456 , delta = 333
我曾经踩过一个坑:在多个线程里同时打桩,忘记加线程ID标识。结果 log 混合在一起,根本分不清哪个事件是哪个线程触发的。所以我现在的习惯是日志里一定带上 Thread.currentThread().name,尤其是 CameraX 内部有 CameraThread、main、binder 等多个线程穿插。

4.2 方法级剖析:TraceView 怎么用才不丢帧

当 adb 打桩找到大概的慢区域后,就该上 TraceView 了。它能告诉你每个方法花了多少 CPU 时间。但要注意——TraceView 的开销不可忽略。

用法很简单:

// 开始追踪
Debug.startMethodTracing("camera_startup")

// 执行你要测量的启动逻辑

// 停止追踪
Debug.stopMethodTracing()

生成的 camera_startup.trace 文件在 /sdcard/ 或 /data/data/包名/files/ 下。用 Android Studio 的 Profiler 打开,或者用 dmtracedump 命令行导出。

重点看两列:Inclusive Time(含子调用) 和 Exclusive Time(自身耗时)。往往你会发现某个看似无辜的 setter 方法独占时间很长——那是因为它内部调用了 CameraX 的异步初始化。

个人经验:我一般先看 Exclusive Time 倒排,把 Top 5 揪出来。如果 Inclusive Time 远大于 Exclusive Time,说明子调用里藏了东西。这时候就把这个方法展开,看看它调了哪些子函数。

4.3 系统级跟踪:Systrace 看线程调度和 Binder 调用

TraceView 只看得到 Java 方法,但很多启动耗时其实卡在系统层面——比如 SurfaceFlinger 没做完、Camera HAL 没返回、或者线程被卡在锁上。这时候 Systrace 就派上用场了。

python systrace.py -t 10 -o camera_startup.html \
  --app=com.example.camera \
  -a com.example.camera \
  -b 8192 \
  sched freq idle am wm gfx view hal dalvik

参数解释:-t 10 抓 10 秒,--app 指定应用,-a 加上 ATrace 标签,-b 缓冲区大小(8MB 够用)。

Systrace 生成的 HTML 用 Chrome 打开。在 Timeline 上找到你的应用进程,放大看启动那一小段。我通常会关注:

  • 黄色片段:CPU 调度延迟。如果线程长时间处于 Runnable 状态但没被运行,说明 CPU 被别的进程抢占了。
  • 绿色细线:Binder 调用。CameraX 初始化会频繁跨进程调用 CameraService,如果 Binder 响应慢,可能相机服务本身就有问题。
  • 锁等待:看到 "Waiting for lock" 的标记,说明两个线程在争同一个锁。
注意:Systrace 在 Android 10+ 上已经标记为 deprecated,Google 推荐用 Perfetto。但我还是习惯在低版本或调试老机器时用 Systrace,因为它的 UI 对线程调度可视化非常直观,而且不需要额外配置。

4.4 现代之选:Perfetto——一站式启动分析

Perfetto 是 Systrace 的升级版,支持 Android 9 及以后。它可以同时抓取 CPU 调度、内存、GPU、文件 I/O、内核事件,甚至能记录调用栈。我个人最近的项目已经全部迁移到 Perfetto 了。

命令行捕获:

adb shell perfetto \
  -c - --txt \
  -o /data/misc/perfetto-traces/camera_startup.perfetto-trace \
  <<< '
write_into_file: true
buffers: { size_kb: 16384 }
data_sources: [
  { config { name: "linux.ftrace" ftrace_config { 
    ftrace_events: [
      "sched/sched_switch",
      "power/cpu_frequency",
      "freq/request",
      "freq/transition"
    ]
  }}}
]
duration_ms: 10000
'

或者更推荐的方式——用 Perfetto UI (ui.perfetto.dev) 上的 Record new trace 按钮,直接在页面勾选需要的 data sources,然后 adb 连接手机一键录制。这会比手写配置简单很多。

我最常用的 Perfetto 功能是 SQL 查询。Trace 打开后,按 Ctrl+Shift+F 进入 SQL 模式,可以写类似 SELECT ts, dur, name FROM slice WHERE name LIKE '%CameraX%' 来精确筛选目标符号。这比用鼠标在时间线上拖拽高效多了。

4.5 四把尺子该怎么选?一张对照表

工具粒度开销能看什么适用场景
adb logcat + 打桩代码级(手动标点)几乎无总耗时、分段耗时快速验证、Release 版本
TraceViewJava 方法级中(2-5x)方法调用耗时、CPU 占用定位具体哪个方法慢
Systrace系统级 + 线程调度低(<10%)进程调度、Binder、锁、SurfaceFlinger看线程是否在等待、系统资源争抢
Perfetto全栈(内核 ~ App)极低(<5%)CPU/GPU/内存/文件 I/O/调用栈线上问题复现、深度性能分析
我踩过另一个坑:在 Release 包上跑 TraceView,结果因为 Tracing 开销导致 ANR。所以我现在严格区分:Debug 包用 TraceView 和 Systrace,Release 包只留 adb 打桩和 Perfetto(开销极小,可以开)。 Perfetto 会抓取系统范围的 ftrace 事件,对 App 本身几乎没有侵入性。

4.6 实战:一次完整的测量流程

假设你刚接手一个 CameraX 模块,启动感觉很慢。我的标准操作是:

  1. 先跑一次 adb + 打桩,得到从 onCreate 到 preview ready 的总耗时。假设是 800ms。
  2. 分段看:发现从 surfaceCreated 到设置预览的输出(Log 中的 5→7)花了 400ms,占比最大。
  3. 上 Perfetto 抓一次完整的启动 Trace,重点关注这个时间段内的 CPU 利用率和线程状态。发现 SurfaceView 回调线程在等待 Binder 返回,而 CameraService 那边正在加载 HAL。
  4. 回头用 TraceView 看 CameraX 内部实现,找到 CameraX 初始化 CameraUseCase 时调用了 CameraXConfig.apply(),里面有一个同步锁占用了 150ms。
  5. 优化锁粒度或者用延迟初始化,改完后用 adb 再次验证总耗时降到 520ms。

嗯,这就是一个完整的闭环。每个工具都在合适的位置发挥了作用,而不是一上来就乱用。

建议你在本地建立一个“启动时间基线”的 CI。每次提交代码后自动跑一次 adb 打桩,在 MR 备注里写 delta。如果 delta 超过 30ms,就需要用 Perfetto 复查。这样可以防止回归不知不觉地溜进来。