2. Activity 启动流程回顾:Application、ActivityThread、WindowManager 等角色

咱们做启动耗时埋点,首先得搞清楚 Activity 到底是怎么“活”起来的。说白了,整个启动流程就是一堆系统组件接力跑,哪个环节慢了,埋点数据就得能揪出来。我早年刚接触性能优化时,觉得 Activity 不就是一个 onCreate 嘛,后来踩了坑才明白——背后套路深着呢。

今天我把核心的几个角色拎出来,一个一个给你拆清楚。

2.1 谁是“总指挥”?—— ActivityThread

如果你在 Android 系统源码里翻过,你会发现 ActivityThread 才是真正干活的。它其实不叫“Thread”,但运行在应用的主线程里。每次启动一个 Activity,都是它收到 AMS(ActivityManagerService)的 IPC 调用。

具体流程是这样的:

  1. AMS 通过 Binder 告诉 ActivityThread:“嘿,给我创建一个 Activity。”
  2. ActivityThread 内部有个 H 类(Handler),把消息分发给主线程 Looper。
  3. 然后它调用 Instrumentation.newActivity() 反射创建 Activity 实例。
  4. 接着走 performLaunchActivity() —— 这一步会调 Activity.attach(),把 Context、Window 等绑定好。
  5. 最后就是 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 图把刚才说的角色和交互画出来了。你盯着看两分钟,流程就全记住了。

Activity启动流程核心角色与交互 AMS ActivityManagerService ActivityThread 主线程调度 + H(Handler) Instrumentation newActivity() Binder IPC 反射创建 Application onCreate() 冷启动时创建 Activity 实例 attach() → onCreate() 反射创建 WindowManager addView() → DecorView ViewRootImpl performTraversals() SurfaceFlinger 合成并送显 → 第一帧 Surface 提交 ⏱ 核心监控点: ① AMS到ActivityThread IPC延迟 ② Application.onCreate 耗时 ③ Activity.onCreate 耗时 ④ 首帧渲染完成时间

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 → 渲染第一帧。下回遇到启动慢的问题,你就能顺着这条链路一个一个查,再加上我们后面要讲的埋点代码实现,肯定能揪出元凶。