21. 日志与监控:启动耗时上报、关键节点打点、异常监控与告警

启动优化做到最后,你会发现一个尴尬的事实——优化做没做对,你自己说了不算,数据说了才算

我个人习惯,在项目启动优化的第一天,就先搭好监控体系。为什么?因为你改了一行代码,启动快了 50ms,这 50ms 是真是假?是稳定复现还是运气好?没有监控,你就是在盲人摸象。

这一章,我们就来聊聊启动优化里的「最后一公里」——日志与监控。说白了,就是三件事:耗时怎么报、节点怎么打、异常怎么抓

启动耗时上报:别只盯着「冷启动」

很多人一提到启动耗时,就只想到冷启动。其实,温启动和热启动的优化空间同样巨大。我在项目中遇到过,某个版本热启动突然慢了 200ms,排查下来是某个后台线程在 Application 里做了不必要的初始化。

所以,上报的时候,我建议你至少区分三种场景:

  • 冷启动:进程被杀死后首次启动
  • 温启动:进程存活,但 Activity 被回收
  • 热启动:进程和 Activity 都在,只是重新 onResume

上报的时机也很关键。我个人习惯在 onWindowFocusChanged 之后,等首帧真正渲染完成再上报。别在 onResume 里报,那个时间点界面可能还是白的。

核心指标:冷启动耗时、温启动耗时、热启动耗时、首帧渲染时间、Application 初始化耗时、ContentProvider 耗时。

关键节点打点:把启动过程「拆开看」

光知道总耗时没用,你得知道时间花在了哪里。我习惯把启动过程拆成几个关键节点,每个节点都打一个时间戳。

举个例子,一个典型的冷启动流程,我会打这些点:

节点名称 打点时机 说明
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_CREATESDK_INIT_DONE 花了 800ms,那你就知道,问题出在 SDK 初始化上。

小技巧:打点工具可以用 System.currentTimeMillis(),也可以用 SystemClock.elapsedRealtime()。后者不受系统时间修改影响,我推荐用它。

异常监控与告警:别等用户骂了才修

启动优化最怕什么?最怕优化没做好,反而搞出了崩溃

我曾经遇到过,为了加快启动速度,把某个 SDK 的初始化放到了子线程。结果那个 SDK 内部用了 Handler,子线程没有 Looper,直接崩溃。线上用户反馈「打开 App 就闪退」,我们排查了整整两天才找到原因。

从那以后,我定了一个规矩:所有启动相关的改动,必须配套异常监控

具体来说,你需要监控这几类异常:

  • 启动崩溃:Application 或 MainActivity 生命周期内的崩溃
  • 启动 ANR:主线程阻塞超过 5 秒
  • 启动超时:冷启动耗时超过某个阈值(比如 5 秒)
  • 关键节点缺失:某个打点没走到,说明流程被中断了

告警的阈值怎么设?我建议分两步走:

  1. 先收集一周的基线数据,看看正常的启动耗时分布
  2. 以 P90 或 P95 作为告警阈值,超过这个值就触发告警

别设得太敏感,否则告警太多,团队会麻木的。嗯,这里要注意,告警的目的是「发现异常」,不是「发现所有波动」。

知识体系:一张图看懂启动监控

说了这么多,我画了一张图,把整个启动监控的流程串起来。你一看就明白了。

Android 启动监控体系 关键节点打点 APP_START ON_CREATE SDK_INIT ACTIVITY FIRST_FRAME 耗时计算与场景分类 冷启动耗时 | 温启动耗时 | 热启动耗时 | 各阶段耗时 数据上报与聚合 实时上报 | 离线缓存 | 批量上传 | 数据清洗 异常监控与告警 启动崩溃 | 启动 ANR | 启动超时 | 节点缺失

代码示例:一个轻量级的打点工具

理论说完了,上点干货。这是我常用的一个打点工具类,轻量、无侵入,你直接拿去用。

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() 上报。

注意:打点代码本身不能影响启动性能。不要在打点里做 IO 操作、网络请求、或者复杂的字符串拼接。否则你就是在「为了监控而拖慢启动」,得不偿失。

避坑指南:我踩过的三个坑

最后,分享三个我亲身踩过的坑,你遇到了可以绕开走。

  • 坑一:打点太多,反而拖慢启动。我曾经在一个项目里打了 30 多个点,结果打点本身占用了 100ms。后来砍到 8 个关键点,问题解决。
  • 坑二:上报时机不对,数据失真。在 onResume 里上报,结果首帧还没渲染完,上报的耗时比实际少了 200ms。后来改到 onWindowFocusChanged 之后上报,数据才准。
  • 坑三:告警阈值设得太低。有一次把冷启动告警阈值设成了 3 秒,结果每天告警几百次,团队直接无视了。后来改成 P95 阈值,告警量降到每天几次,每次都是真问题。

嗯,监控这件事,说白了就是「用数据说话」。没有监控,你做的优化就是空中楼阁。有了监控,你才能知道每一步优化到底有没有效果,有没有引入新的问题。

这一章的内容就到这里。记住:优化是手段,监控是保障。两者缺一不可。


公众号:蓝海资料掘金营,微信deep3321