全景:一张图看完整场接力
先不抠细节,把冷启动(目标进程还不存在)的完整路线一次看完。四条泳道就是四个角色,竖直方向是时间。记住这张图,后面每一节都只是把其中一程放大。
冷启动全流程 · 四条泳道
tap → fork → onCreate → 首帧为什么要绕这么一大圈
新手最自然的想象是:startActivity() ≈ new TargetActivity() 然后显示出来。理解「为什么不能这么干」,整个流程就不用死记了——每一步都是被进程隔离逼出来的。
startActivity就是 new 一个对象再 show 出来。- 目标页面和我在同一个世界里,直接调用就行。
- 启动慢,大概是页面本身太重。
- 它是一次跨进程 RPC:目标 Activity 可能属于另一个 App、另一个进程,你既无权也无法直接 new 它。
- 窗口、任务栈、权限是全局资源,必须有个裁判(system_server)统一裁决。
- 冷启动慢在 fork 进程 + Application 初始化 + 首帧渲染,不只是页面重。
startActivity() 理解成前端发了一个 HTTP 请求:system_server 是服务端,负责鉴权和调度;生命周期回调是服务端推回来的事件。只是这一切被 Binder 代理包装成了「本地函数调用」的样子——写起来像调函数,跑起来是跨进程。ATMS · 调度台
ActivityTaskManagerService,住在 system_server 进程里,是全系统 Activity 的裁判:验票(权限)、排座(任务栈)、发号施令(生命周期)。Android 10 之前这活儿归 AMS,理解时当一回事即可。
Zygote · 孵化器
系统开机时就把 JVM(ART)+ 常用类 + 资源预加载好的一个进程模板。所有 App 进程都由它 fork 而来——写时复制,不用从零起 VM,又快又省内存。
ActivityThread · 管家
每个 App 进程的主线程管家。它的 main() 是 App 进程的入口:启动 Looper 消息循环,之后 ATMS 的每道指令都变成消息,由它在主线程执行成生命周期回调。注意:它不是一个线程。
第一程:发起端,从一行代码到 Binder 出口
在你的进程里,这一程很短:层层转手,最后把请求交给 ATMS 的 Binder 代理扔出进程。值得认识的只有一个中间人——Instrumentation。
前两步只是重载转发;真正干活的是 Instrumentation,最后一步拿到 ATMS 在本进程的代理对象,发起跨进程调用——请求就此离开你的进程。
Instrumentation · 仪表台
启动与生命周期的必经关卡每个 App 进程里有唯一一个 Instrumentation 实例,所有 Activity 的启动和每个生命周期回调都从它手上过一遍。平时它只是透明转发;做自动化测试时,测试框架就是把它换掉,才能「监控/拦截」整个 App——这也是它名字(仪表)的由来。
你熟悉的报错 Have you declared this activity in your AndroidManifest.xml? 就是它在 checkStartActivityResult() 里抛的:ATMS 返回「查无此人」,它负责翻译成人话。
public ActivityResult execStartActivity(...) { // 拿到 ATMS 在本进程的 Binder 代理,跨进程调用 int result = ActivityTaskManager.getService() .startActivity(whoThread, intent, ...); // 启动失败 → 在这里抛出你熟悉的那些异常 checkStartActivityResult(result, intent); }
ActivityTaskManager.getService() 返回的不是 ATMS 本尊,而是它在你进程里的 Binder 代理——对代理调方法,参数被打包送到 system_server,在那边的 ATMS 真身上执行。「像函数调用的 RPC」,这就是 Binder。第二程:ATMS 当裁判,Zygote 生进程
请求进了 system_server,ATMS 开始真正的调度。它自己不执行任何生命周期——它只做决定,然后向各 App 进程发号施令。
① 验票与找人
拿 intent 去问 PackageManagerService:目标到底是谁?再查权限、exported、启动限制。没在 Manifest 注册的,这一步直接打回。
② 排座次
按 launchMode 和 Task 栈现状决定:复用已有实例,还是压一个新的进栈?复用的话走 onNewIntent(),后面的创建流程就省了。
③ 保进程
确认目标进程活着:活着就直接下发指令(热/温启动);不在,才去找 Zygote fork(冷启动)。「每次启动都开新进程」是误解。
目标进程不存在时,ATMS 通过 socket(注意,这一步不走 Binder)请求 Zygote。Zygote fork 一份自己:新进程天生带着预加载好的 ART 虚拟机和框架类,起点极高。fork 出来后第一件事,就是执行 ActivityThread.main()——
ActivityThread.main() · App 进程的入口
你的 App 从这里开始活很多人以为 App 的入口是 Application.onCreate(),再往前其实是这里:准备主线程 Looper → 向 ATMS attach 报到 → 进入死循环收消息。从此这个进程的一生,就是主线程不断从消息队列里取指令执行。
attach 报到后,ATMS 回发 bindApplication,主线程借此创建 Application 并回调它的 onCreate()——所以 Application.onCreate 一定早于任何 Activity,冷启动优化第一刀也总是砍向这里的初始化。
public static void main(String[] args) { Looper.prepareMainLooper(); // 主线程消息循环 ActivityThread thread = new ActivityThread(); thread.attach(false, startSeq); // 向 ATMS 报到 Looper.loop(); // 死循环,取消息干活 // 走到这行 = 主线程退出 = 进程该死了 throw new RuntimeException("Main thread loop unexpectedly exited"); }
两头是重点:A 的 onPause 挡在 B 的最前面(见上面的顺序陷阱);A 的 onStop 要等 B 画完首帧才执行——系统保证屏幕上始终有东西可看。
第三程:回到目标进程,实例出生
ATMS 打包一个 ClientTransaction(「去启动这个 Activity」)发给目标进程。指令先落在 Binder 线程上,但生命周期必须在主线程跑——于是有了那次著名的「切线程」。
目标进程内部:从 Binder 线程到首帧
Binder thread → H → main threadperformLaunchActivity · 实例的出生地
你的 Activity 在这里被 new 出来三件事:反射创建实例(所以 Activity 不能写有参构造,你也永远不该自己 new 它)、attach(配上 Context 和 PhoneWindow,Activity 从「裸对象」变成「有窗户的页面」)、最后经 Instrumentation 回调 onCreate()——你写的代码到这一刻才登场。
注意 setContentView() 只是把 XML 解析成 View 树挂到窗口上,并不渲染。真正上屏要等 onResume 之后 WindowManager.addView() 把 ViewRootImpl 接进来,走完 measure / layout / draw 才有第一帧。
// ① 反射创建 —— 不是你 new 的 Activity a = mInstrumentation.newActivity( classLoader, component.getClassName(), r.intent); // ② attach:配上 Context 和 PhoneWindow a.attach(appContext, this, getInstrumentation(), r.token, app, r.intent, r.activityInfo, ...); // ③ 回调 onCreate —— 你的代码从这里登场 mInstrumentation.callActivityOnCreate(a, r.state);
H.LAUNCH_ACTIVITY 消息;Android 10 起 Activity 调度从 AMS 拆出成 ATMS。网上博客新旧混杂,类名对不上时先看它写的是哪个版本——骨架流程从未变过。易混点与上手
几个最容易栽的认知坑,一张 launchMode 速查表,冷温热启动的区别,以及怎么亲眼验证这条链路。
| 常见误解 | 实际情况 |
|---|---|
| startActivity 就是 new 一个目标 Activity 再显示。 | 它是一次跨进程 RPC:请求绕道 system_server,由 ATMS 调度,最后在目标进程反射创建——全程你没有机会亲手 new。 |
| ActivityThread 是一个线程。 | 它是个普通 Java 类,名字骗人。它的 main() 跑在主线程上、管理主线程消息循环——是“主线程的管家”,不是线程本身。 |
| setContentView 之后界面就画出来了。 | 它只是把 XML 解析成 View 树。真正渲染在 onResume 之后:addView → ViewRootImpl → measure/layout/draw,才有首帧。onCreate/onResume 里量不到宽高也是这个原因。 |
| 先启动新页面,再暂停旧页面。 | 反了:旧 Activity 先 onPause,新的才 onCreate,而且 ATMS 会等 onPause 执行完。onPause 里做重活,拖慢的是下一个页面。 |
| 每次 startActivity 都会新开一个进程。 | 进程存在就直接复用,只有冷启动才劳驾 Zygote fork。同理,Activity 实例是否复用由 launchMode 决定,和进程是两码事。 |
| ATMS 和 App 之间所有通信都走 Binder。 | 几乎都是,唯独 ATMS → Zygote 走 socket。一个常见解释:fork 不希望发生在多线程的 Binder 环境里(fork 只复制当前线程,锁状态易错乱),socket 单线程收发更安全。 |
| 维度 | standard | singleTop | singleTask | singleInstance |
|---|---|---|---|---|
| 何时复用 | 从不,每次都新建 | 目标已在栈顶才复用 | 栈内已有就复用,并清掉它上面的 | 全系统仅一个,独占一个栈 |
| 复用时的回调 | —(总走 onCreate) | onNewIntent() | onNewIntent() | onNewIntent() |
| 典型场景 | 绝大多数页面 | 通知点开的消息页 | App 首页 | 来电 / 闹钟页 |
冷启动 Cold
进程不存在,走全景图全程:Zygote fork → bindApplication → Application.onCreate → Activity → 首帧。最慢,启动优化主要就是优化它。
温启动 Warm
进程还在,Activity 没了(被销毁/回收)。跳过 fork 和 Application 初始化,从全景图第 ⑥ 步开始重建 Activity。
热启动 Hot
进程和 Activity 都在,只是切回前台。基本只走 onRestart → onStart → onResume,最快。
第 1 步 · 量启动
终端跑 adb shell am start -W 包名/.MainActivity,看输出的 TotalTime。先冷启动测一次,back 退出再测(热),对比数字体感三种启动的差距。
第 2 步 · 看脚印
在 Application.onCreate 和两个 Activity 的各生命周期里打 Log,A 跳 B 观察顺序——亲眼确认 A.onPause 先于 B.onCreate、A.onStop 最后。
第 3 步 · 看主线程
用 Android Studio Profiler(或 Perfetto)抓一次冷启动,在主线程时间轴上找到 bindApplication、onCreate、首帧 doFrame——全景图就活生生躺在那条线上。