1. Activity启动耗时简介:什么是启动耗时,为何需要埋点

大家好,我是老张。今天开始咱们聊聊Activity启动耗时的埋点。说句实话,这玩意儿我做了好几年,踩过的坑能写一本书。你想想看,一个App启动慢半秒,用户可能就直接划走了。所以这节课,咱们先把基础打牢。

什么是启动耗时?

启动耗时,说白了就是从用户点击图标那一刻,到App首帧完全展示、用户能正常操作的那段时间。注意,是「用户能交互」才算完,光看到闪屏不算。

我当初刚做性能优化时,以为只要Activity.onCreate()执行完就算启动结束。后来被产品怼了一脸——屏幕还在那转菊花,用户点不动,你跟我说启动完了?嗯,从那以后我就记住了:启动结束的标志是「首帧绘制完成 + 用户可点击」。

启动时长的三个类型

咱们行业里一般把启动分成三类,每一类的耗时组成完全不同。我整理了一个表格,方便你对照着看。

类型 触发场景 进程状态 典型耗时
冷启动 进程首次创建 无进程 1~3秒(正常App)
温启动 进程存活,Activity被销毁 后台存活 200~600ms
热启动 进程存活,Activity仍在栈中 完全活跃 100~300ms

冷启动是最重的,也是咱们埋点重点关注的。温启动次之。热启动嘛,如果超过500ms,那就有问题了。

记住一点:冷启动的耗时大头在Application.onCreate()和ContentProvider初始化。我建议埋点时就盯着这两块,基本能抓住80%的问题。

为什么需要埋点?

你可能觉得,「我开发时测一下启动时间不就行了?」——太天真了。真实用户的环境千奇百怪:低端机、后台一堆App、网络慢、文件读写卡……你开发机跑得飞快,一上线就崩。

我个人习惯是:没有埋点的性能优化都是瞎搞。为什么?因为你需要数据说话。

  • 量化问题:启动到底慢不慢?慢了多少?哪个阶段最慢?
  • 监控波动:版本发布后,启动时间有没有变差?
  • 定位根因:比如突然慢了500ms,是某个第三方SDK升级导致的,还是自家代码改坏了?
  • 验证优化效果:改了代码,启动快了多少?老板要报表,你得拿出数据。

我之前在一个电商项目里,就是靠埋点发现某个启动阶段在低端机上耗时暴增。查了半天,原来是SharedPreferences里存了个巨大的JSON字符串。嗯,这种坑只有线上埋点才能抓到。

启动耗时的组成

为了搞清楚埋点埋在哪,你得先知道启动过程中发生了什么。我画了一张流程图,把关键阶段标出来了。

Activity启动耗时关键阶段 用户点击图标 创建进程 + Application ContentProvider / SDK初始化 onCreate / onStart 首帧绘制(ReportFullyDrawn) 用户可交互 说明: 冷启动会经过全部6个阶段,温启动跳过前两个进程创建阶段,热启动只走4->5->6。 埋点建议在阶段3(初始化结束)、阶段5(首帧绘制)设打点,就能算出核心耗时。

从图上你能看到,启动其实是个串行流水线。每个阶段都可能成为瓶颈。我建议你重点关注这几个地方:

  1. Application.onCreate()—— 第三方SDK初始化别放这,能懒加载就懒加载。
  2. ContentProvider#onCreate()—— 坑中之坑,每个ContentProvider都延后启动时间。
  3. Activity.onCreate()中的setContentView和findViewById—— 布局层级越深,inflate越慢。
  4. 首帧绘制时间—— 只有这个才是用户真正看到的东西。

避坑指南:埋点之前别踩的雷

我曾经在一个项目里直接在主线程打印Log来记启动耗时,结果Log本身拖慢了启动。后来一测,Log写入I/O花了200ms。记住:埋点代码本身不能成为性能负担。

另外,启动耗时受太多因素影响。你知道同一台手机,连着充电器和拔掉电源,启动时间能差30%吗?还有系统版本、CPU降频、磁盘碎片……所以埋点时一定要去噪。

  • 不要只看单次数据,要统计P50、P95、P99。
  • 区分冷/温/热启动,分别上报。
  • 记录设备信息(型号、系统、存储剩余)。

小结:你到底需要埋哪些点?

如果你现在要开始做一个启动埋点,我的建议是这样的:

核心打点位置(最少必须埋):

  • t0:Application.attachBaseContext() 开始 —— 进程创建时间
  • t1:Application.onCreate() 结束 —— 应用初始化完成
  • t2:Activity.onCreate() 开始 —— Activity创建
  • t3:Activity.onResume() 结束 —— 生命周期完成
  • t4:首帧绘制回调(ViewTreeObserver.OnDrawListener 或 ReportFullyDrawn)

这样你就能算出:
进程创建耗时 = t1 - t0
Activity创建耗时 = t3 - t2
首帧绘制耗时 = t4 - t3
总启动耗时 = t4 - t0

为什么没有把用户点击那一刻作为t0?因为从点击到进程创建这段时间,你是拿不到的(系统控制的)。所以t0实际上是进程创建开始,这个大家统一就行。