启动耗时测量方法:四种主流手段
说到启动速度监控,第一步肯定是「怎么量」。
我刚开始做性能优化时,拿个秒表在那点,后来被同事笑话了……其实测量方法不少,但每种都有自己的脾气。今天我就把我常用的四种方式掰开揉碎讲清楚。
1. 手动埋点 — 最原始也最灵活
说白了就是在代码里插桩。比如 Application.onCreate() 开头记个时间,Activity.onCreate() 开头再记一个,一减就是差值。
// 手动埋点示例
public class MyApp extends Application {
@Override
public void onCreate() {
long start = System.nanoTime();
super.onCreate();
// … 各种初始化
long end = System.nanoTime();
Log.d("AppStart", "Application.onCreate() 耗时: " + (end - start) / 1_000_000 + " ms");
}
}
手动埋点的好处是「想测哪里测哪里」。我在项目中遇到过第三方 SDK 初始化特别慢的问题,就是靠逐行埋点锁定的。但缺点也明显——代码侵入性强,线上版本还得想办法关掉。
2. System.currentTimeMillis() — 小心时钟回拨
这个方法很多人拿来记启动总耗时。但你知道吗?系统时间可能被 NTP 同步、用户手动修改甚至回拨。你想想看,如果中间时间回退了,差值变成负数,你找谁哭去?
所以我的建议是:只用来做粗略的「墙上时间」统计,千万别用它做精确性能对比。
// 不推荐的做法
long start = System.currentTimeMillis();
// 业务逻辑
long end = System.currentTimeMillis();
long cost = end - start; // 假如中间时间被改了?
为什么说它包含线程等待时间?因为 currentTimeMillis 反映的是真实世界流逝,你的线程被调度出去干等了 200 ms,这个时间也算进去了。这其实是好事——用户感知的「慢」就是这个。
3. Process.getElapsedCpuTime() — 真实CPU消耗
这是 Android(API 1 起就有)的一个隐藏技能。它返回进程占用的 CPU 时间,单位是纳秒。注意:仅仅是进程自己的 CPU 执行时间,不包括线程挂起、IO 等待、GC 暂停等。
// 获取CPU耗时
long cpuStart = Process.getElapsedCpuTime();
// 需要测量的工作
long cpuEnd = Process.getElapsedCpuTime();
Log.d("CPU_trace", "CPU实际运行: " + (cpuEnd - cpuStart) / 1_000_000 + " ms");
这个值在线上很好用,因为它不依赖系统时间。我记得有个 Case:启动总耗时 800 ms,用 CPU 时间一算只有 200 ms,说明剩下 600 ms 都是在等 IO 或锁。一下子就找到了方向。
4. 启动日志时间戳 — 系统级基准线
Android 系统会在启动时输出很多日志,例如:
01-01 12:00:00.123 I/ActivityManager: Start proc 10086:com.example/u0a123 for activity ...
01-01 12:00:01.456 I/ActivityManager: Displayed com.example/.MainActivity: +1s332ms
「Displayed」这个日志就是官方钦定的启动完成时刻。你可以在代码里用 adb logcat -b events | grep "am_activity_launch_time" 拿到结构化的时间戳。也可以直接在 logcat 里 grep 出来。
我经常拿它当基准:Displayed 时间减去进程创建时间(Start proc 那一行)就是用户感知的冷启动总耗时。然后跟我手动埋点的分段时间做 cross check,对不上就说明有地方漏了或者多了。
四种方法对比表
| 方法 | 精度 | 是否受系统时间影响 | 线上可用性 | 推荐场景 |
|---|---|---|---|---|
| 手动埋点 | 纳秒级 | 否(用nanoTime) | 需剥离 | 细化定位瓶颈 |
| System.currentTimeMillis() | 毫秒级 | 是 | 可以 | 粗略总耗时 |
| Process.getElapsedCpuTime() | 纳秒级 | 否 | 可以 | CPU开销分析 |
| 启动日志时间戳 | 毫秒级 | 是(受系统time影响) | 天然可用 | 总耗时基准线 |
实操建议
- 先搭日志框架: 在 Application 启动时开启 Log.isLoggable 开关,避免线上日志污染。
- 多仪联测: 同时记录 currentTimeMillis 和 getElapsedCpuTime,比值能告诉你「到底是在等还是真的在跑代码」。
- 注意线程安全: 埋点的起始和结束点一定在同一个线程内,否则差值没意义。
- 统计多轮: 冷启动建议连续采集 10~20 次数据,取中位数或 P50、P95,单次偶然性太大。
好了,四种方法都过了一遍。你想想看,实际开发中可能不止用一种。我个人习惯是:线上用日志时间戳 + CPU time 做大盘监控,线下用埋点做精确定位。