4. 相机启动时间测量
做启动优化,第一件事就是把“感觉慢”变成“具体数字”。我见过太多开发者一上来就改代码,改完也不知道快了多少——这其实是在盲调。今天我们来聊聊怎么用 adb、TraceView、Systrace 和 Perfetto 这四把尺子,把 CameraX 启动的每一毫秒都量清楚。
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.123 | CameraStartup | 1: onStart at 1715506425123 |
| 05-12 14:23:45.456 | CameraStartup | 9: preview ready at 1715506425456 , delta = 333 |
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 的异步初始化。
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" 的标记,说明两个线程在争同一个锁。
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 连接手机一键录制。这会比手写配置简单很多。
SELECT ts, dur, name FROM slice WHERE name LIKE '%CameraX%' 来精确筛选目标符号。这比用鼠标在时间线上拖拽高效多了。
4.5 四把尺子该怎么选?一张对照表
| 工具 | 粒度 | 开销 | 能看什么 | 适用场景 |
|---|---|---|---|---|
| adb logcat + 打桩 | 代码级(手动标点) | 几乎无 | 总耗时、分段耗时 | 快速验证、Release 版本 |
| TraceView | Java 方法级 | 中(2-5x) | 方法调用耗时、CPU 占用 | 定位具体哪个方法慢 |
| Systrace | 系统级 + 线程调度 | 低(<10%) | 进程调度、Binder、锁、SurfaceFlinger | 看线程是否在等待、系统资源争抢 |
| Perfetto | 全栈(内核 ~ App) | 极低(<5%) | CPU/GPU/内存/文件 I/O/调用栈 | 线上问题复现、深度性能分析 |
4.6 实战:一次完整的测量流程
假设你刚接手一个 CameraX 模块,启动感觉很慢。我的标准操作是:
- 先跑一次 adb + 打桩,得到从 onCreate 到 preview ready 的总耗时。假设是 800ms。
- 分段看:发现从 surfaceCreated 到设置预览的输出(Log 中的 5→7)花了 400ms,占比最大。
- 上 Perfetto 抓一次完整的启动 Trace,重点关注这个时间段内的 CPU 利用率和线程状态。发现 SurfaceView 回调线程在等待 Binder 返回,而 CameraService 那边正在加载 HAL。
- 回头用 TraceView 看 CameraX 内部实现,找到 CameraX 初始化 CameraUseCase 时调用了
CameraXConfig.apply(),里面有一个同步锁占用了 150ms。 - 优化锁粒度或者用延迟初始化,改完后用 adb 再次验证总耗时降到 520ms。
嗯,这就是一个完整的闭环。每个工具都在合适的位置发挥了作用,而不是一上来就乱用。