2. Activity 启动流程回顾:Application、ActivityThread、WindowManager 等角色
咱们做启动耗时埋点,首先得搞清楚 Activity 到底是怎么“活”起来的。说白了,整个启动流程就是一堆系统组件接力跑,哪个环节慢了,埋点数据就得能揪出来。我早年刚接触性能优化时,觉得 Activity 不就是一个 onCreate 嘛,后来踩了坑才明白——背后套路深着呢。
今天我把核心的几个角色拎出来,一个一个给你拆清楚。
2.1 谁是“总指挥”?—— ActivityThread
如果你在 Android 系统源码里翻过,你会发现 ActivityThread 才是真正干活的。它其实不叫“Thread”,但运行在应用的主线程里。每次启动一个 Activity,都是它收到 AMS(ActivityManagerService)的 IPC 调用。
具体流程是这样的:
- AMS 通过 Binder 告诉
ActivityThread:“嘿,给我创建一个 Activity。” ActivityThread内部有个H类(Handler),把消息分发给主线程 Looper。- 然后它调用
Instrumentation.newActivity()反射创建 Activity 实例。 - 接着走
performLaunchActivity()—— 这一步会调Activity.attach(),把 Context、Window 等绑定好。 - 最后就是
Activity.onCreate()、onStart()、onResume()那套生命周期了。
所以,如果你要埋点,ActivityThread 的执行时机就是最上游的监控点。我个人习惯在 performLaunchActivity 入口打个时间戳,这样能抓到系统调度的延迟。
关键埋点时机:
ActivityThread.performLaunchActivity()开始 — 启动起点Activity.attach()完成 — 窗口绑定完毕Activity.onCreate()开始 — 业务代码执行起点Activity.onResume()结束 — 界面可见
2.2 应用的总管家 —— Application
启动任何 Activity 之前,Application 必须先创建。如果你在 Application.onCreate() 里做了太多初始化,比如加载 SDK、初始化数据库,那第一个 Activity 的启动就得等着。我曾经在一个项目里遇到过:App 启动后点首页总卡 3 秒,一查发现 Application.onCreate() 里做了大量网络预请求——这不就是自己给自己挖坑吗?
这里有个重要概念:冷启动 和 温启动。
| 启动类型 | 是否创建 Application | Activity 实例状态 | 典型耗时 |
|---|---|---|---|
| 冷启动 | 是 | 全新创建 | 最长,包含进程创建 + Application 初始化 |
| 温启动 | 否(已存在) | 可能重建 | 较短 |
| 热启动 | 否 | 从后台回前台,Activity 不被销毁 | 极短 |
埋点的时候一定要区分启动类型,否则数据会失真。你想想看,如果冷启动和热启动混在一起算平均,性能问题根本揪不出来。
2.3 窗口的调度者 —— WindowManager 与 ViewRootImpl
Activity 创建之后,系统还得把它的 UI 画出来。这里面最核心的角色是 WindowManager 和 ViewRootImpl。
- WindowManager:负责管理 Window 的添加、更新、删除。每个 Activity 对应一个 PhoneWindow,PhoneWindow 通过
WindowManager.addView()把 DecorView 挂上去。 - ViewRootImpl:这才是真正的渲染调度者。它持有 Surface,负责跟 SurfaceFlinger 通信。每一次
performTraversals()都会触发 measure、layout、draw。
所以启动耗时不仅包括 Activity 生命周期代码,还包括第一次布局和绘制的时间。我一般会在 ViewRootImpl.setView() 之后记录第一帧的开始,然后配合 Choreographer 监听第一帧完成的时间——这就是所谓的“首帧耗时”。
避坑指南(我曾经踩过): 有些人只埋 onResume 的结束时间,以为界面显示完成了。但那时候可能还没真正绘出一帧,屏幕还是黑的。一定要结合第一帧的 callback 来确认。
2.4 整体流程框架图
下面这张 SVG 图把刚才说的角色和交互画出来了。你盯着看两分钟,流程就全记住了。
2.5 生命周期中的关键时间点(埋点锚点)
搞清楚了角色,我们把启动过程拆成几个时间点:
- T0:用户点击图标 / 系统发起启动 Intent —— 这个我们拿不到精确时间,但可以用
Process.getStartElapsedRealtime()近似。 - T1:Application 构造函数执行 —— 冷启动信号。
- T2:
Activity.attach()完成 —— 窗口已绑定。 - T3:
Activity.onCreate()执行完毕。 - T4:
Activity.onResume()执行完毕 —— 界面可交互(但可能还没绘制)。 - T5:第一帧绘制完成(通过
Choreographer或reportFullyDrawn)。
我建议至少埋这些点,然后再算几个关键差值:
| 差值指标 | 含义 | 常见瓶颈 |
|---|---|---|
| T1 – T0 | 进程创建 + 系统调度延迟 | 低内存、系统负载高 |
| T2 – T1 | Application 初始化耗时 | SDK 初始化、ContentProvider |
| T3 – T2 | Activity 构建 + 布局 inflate 耗时 | XML 解析、构造函数中做耗时操作 |
| T4 – T3 | onResume 阶段业务代码 | 动画、网络请求、数据库查询 |
| T5 – T4 | 首帧渲染耗时 | 过度绘制、复杂布局、动画 |
| T5 – T0 | 总启动耗时(冷启动) | 综合优化 |
一点小经验: 我在做埋点框架时,会尽量在 LAUNCH_ACTIVITY 这个系统发送的 Message 时间戳上做文章。你可以 Hook ActivityThread.mH 的 Message 队列,直接获取调度到主线程的时间——这样 T0 就更精确了。
好了,现在你脑子里应该有一幅清晰的画面了:AMS 发指令 → ActivityThread 接活 → 创建 Application → 创建 Activity → 绑定 Window → 渲染第一帧。下回遇到启动慢的问题,你就能顺着这条链路一个一个查,再加上我们后面要讲的埋点代码实现,肯定能揪出元凶。