21. 日志与监控:启动耗时上报、关键节点打点、异常监控与告警
启动优化做到最后,你会发现一个尴尬的事实——优化做没做对,你自己说了不算,数据说了才算。
我个人习惯,在项目启动优化的第一天,就先搭好监控体系。为什么?因为你改了一行代码,启动快了 50ms,这 50ms 是真是假?是稳定复现还是运气好?没有监控,你就是在盲人摸象。
这一章,我们就来聊聊启动优化里的「最后一公里」——日志与监控。说白了,就是三件事:耗时怎么报、节点怎么打、异常怎么抓。
启动耗时上报:别只盯着「冷启动」
很多人一提到启动耗时,就只想到冷启动。其实,温启动和热启动的优化空间同样巨大。我在项目中遇到过,某个版本热启动突然慢了 200ms,排查下来是某个后台线程在 Application 里做了不必要的初始化。
所以,上报的时候,我建议你至少区分三种场景:
- 冷启动:进程被杀死后首次启动
- 温启动:进程存活,但 Activity 被回收
- 热启动:进程和 Activity 都在,只是重新 onResume
上报的时机也很关键。我个人习惯在 onWindowFocusChanged 之后,等首帧真正渲染完成再上报。别在 onResume 里报,那个时间点界面可能还是白的。
关键节点打点:把启动过程「拆开看」
光知道总耗时没用,你得知道时间花在了哪里。我习惯把启动过程拆成几个关键节点,每个节点都打一个时间戳。
举个例子,一个典型的冷启动流程,我会打这些点:
| 节点名称 | 打点时机 | 说明 |
|---|---|---|
| APP_START | Application.attachBaseContext | 进程启动的起点 |
| APP_ON_CREATE | Application.onCreate 开始 | Application 初始化入口 |
| SDK_INIT_DONE | 所有 SDK 初始化完成 | 第三方库加载完毕 |
| ACTIVITY_CREATE | MainActivity.onCreate | 主界面开始创建 |
| ACTIVITY_RESUME | MainActivity.onResume | 界面可见 |
| FIRST_FRAME | 首帧渲染完成 | 用户真正看到内容 |
每个节点之间,就是一段可优化的时间窗口。比如 APP_ON_CREATE 到 SDK_INIT_DONE 花了 800ms,那你就知道,问题出在 SDK 初始化上。
异常监控与告警:别等用户骂了才修
启动优化最怕什么?最怕优化没做好,反而搞出了崩溃。
我曾经遇到过,为了加快启动速度,把某个 SDK 的初始化放到了子线程。结果那个 SDK 内部用了 Handler,子线程没有 Looper,直接崩溃。线上用户反馈「打开 App 就闪退」,我们排查了整整两天才找到原因。
从那以后,我定了一个规矩:所有启动相关的改动,必须配套异常监控。
具体来说,你需要监控这几类异常:
- 启动崩溃:Application 或 MainActivity 生命周期内的崩溃
- 启动 ANR:主线程阻塞超过 5 秒
- 启动超时:冷启动耗时超过某个阈值(比如 5 秒)
- 关键节点缺失:某个打点没走到,说明流程被中断了
告警的阈值怎么设?我建议分两步走:
- 先收集一周的基线数据,看看正常的启动耗时分布
- 以 P90 或 P95 作为告警阈值,超过这个值就触发告警
别设得太敏感,否则告警太多,团队会麻木的。嗯,这里要注意,告警的目的是「发现异常」,不是「发现所有波动」。
知识体系:一张图看懂启动监控
说了这么多,我画了一张图,把整个启动监控的流程串起来。你一看就明白了。
代码示例:一个轻量级的打点工具
理论说完了,上点干货。这是我常用的一个打点工具类,轻量、无侵入,你直接拿去用。
public class LaunchTracker {
private static final String TAG = "LaunchTracker";
private static final Map<String, Long> sTimestamps = new LinkedHashMap<>();
public static void mark(String key) {
long now = SystemClock.elapsedRealtime();
sTimestamps.put(key, now);
Log.d(TAG, key + " : " + now);
}
public static long getDuration(String startKey, String endKey) {
Long start = sTimestamps.get(startKey);
Long end = sTimestamps.get(endKey);
if (start == null || end == null) {
return -1;
}
return end - start;
}
public static void report() {
// 这里把数据上报到你的监控平台
long coldStart = getDuration("APP_START", "FIRST_FRAME");
long sdkInit = getDuration("APP_ON_CREATE", "SDK_INIT_DONE");
// ... 上报逻辑
}
}
用法很简单:在关键节点调用 LaunchTracker.mark("节点名"),最后在首帧渲染完成后调用 report() 上报。
避坑指南:我踩过的三个坑
最后,分享三个我亲身踩过的坑,你遇到了可以绕开走。
- 坑一:打点太多,反而拖慢启动。我曾经在一个项目里打了 30 多个点,结果打点本身占用了 100ms。后来砍到 8 个关键点,问题解决。
- 坑二:上报时机不对,数据失真。在
onResume里上报,结果首帧还没渲染完,上报的耗时比实际少了 200ms。后来改到onWindowFocusChanged之后上报,数据才准。 - 坑三:告警阈值设得太低。有一次把冷启动告警阈值设成了 3 秒,结果每天告警几百次,团队直接无视了。后来改成 P95 阈值,告警量降到每天几次,每次都是真问题。
嗯,监控这件事,说白了就是「用数据说话」。没有监控,你做的优化就是空中楼阁。有了监控,你才能知道每一步优化到底有没有效果,有没有引入新的问题。
这一章的内容就到这里。记住:优化是手段,监控是保障。两者缺一不可。