Activity 是如何启动

startActivity() 不是简单地 new 一个页面,而是一场跨进程调度。请求先进入 system_server,由 ATMS 决定任务栈和目标进程,再回到应用主线程创建 Activity、执行生命周期并绘制首帧。

01
发起进程通过 Binder 提交启动请求
03
目标进程主线程创建页面并绘制首帧
一次页面启动,是发起进程、系统进程和目标进程之间的接力。
— 00

全景:一张图看完整场接力

先不抠细节,把冷启动(目标进程还不存在)的完整路线一次看完。四条泳道就是四个角色,竖直方向是时间。记住这张图,后面每一节都只是把其中一程放大。

冷启动全流程 · 四条泳道

tap → fork → onCreate → 首帧
Activity 冷启动四泳道流程:发起进程请求 ATMS,ATMS 暂停旧页面并安排任务栈;必要时由 Zygote 创建目标进程,最后在目标进程主线程创建 Activity 并绘制首帧。 发起进程 · Launcher/你的App system_server · ATMS Zygote 目标进程(新) startActivity() 你的代码只有这一行 ATMS 受理请求 校验 intent · 权限 · exported 安排任务栈 launchMode:复用还是新建 旧 Activity onPause() 所以 onPause 别做重活 目标进程存在? 已存在 → 直接跳 ⑥ Zygote fork 自己 预热好的进程模板 新进程诞生 ActivityThread.main() 主线程 Looper 转起来 向 ATMS attach 报到 回发 bindApplication → Application.onCreate() 下发启动事务 ClientTransaction H 切到主线程 生命周期都在主线程执行 反射 new → onCreate onStart → onResume → 首帧上屏 旧 Activity onStop() 新页面可见后才轮到它 你的代码只有 ① 那一行 —— 其余全是系统在三个进程之间的接力。
① 发起方经 Binder 把请求交给 system_server 里的 ATMS  ;· ; ② ATMS 先让台上的旧 Activity onPause  ;· ; ③ 目标进程不存在时,经 socket(不是 Binder)请 Zygote fork  ;· ; ④ 新进程从 ActivityThread.main() 开始活  ;· ; ⑤ 新进程报到,ATMS 回发 bindApplication → Application.onCreate()  ;· ; ⑥ 下发 ClientTransaction,主线程创建 Activity 走生命周期  ;· ; ⑦ 首帧可见后,旧 Activity 才 onStop
— 01

为什么要绕这么一大圈

新手最自然的想象是:startActivity()new TargetActivity() 然后显示出来。理解「为什么不能这么干」,整个流程就不用死记了——每一步都是被进程隔离逼出来的。

你以为的
  • startActivity 就是 new 一个对象再 show 出来。
  • 目标页面和我在同一个世界里,直接调用就行。
  • 启动慢,大概是页面本身太重
实际上
  • 它是一次跨进程 RPC:目标 Activity 可能属于另一个 App、另一个进程,你既无权也无法直接 new 它。
  • 窗口、任务栈、权限是全局资源,必须有个裁判(system_server)统一裁决。
  • 冷启动慢在 fork 进程 + Application 初始化 + 首帧渲染,不只是页面重。
给写过 Web 的你 ·startActivity() 理解成前端发了一个 HTTP 请求:system_server 是服务端,负责鉴权和调度;生命周期回调是服务端推回来的事件。只是这一切被 Binder 代理包装成了「本地函数调用」的样子——写起来像调函数,跑起来是跨进程。
先认识三个陌生角色 cast of characters

ATMS · 调度台

ActivityTaskManagerService,住在 system_server 进程里,是全系统 Activity 的裁判:验票(权限)、排座(任务栈)、发号施令(生命周期)。Android 10 之前这活儿归 AMS,理解时当一回事即可。

Zygote · 孵化器

系统开机时就把 JVM(ART)+ 常用类 + 资源预加载好的一个进程模板。所有 App 进程都由它 fork 而来——写时复制,不用从零起 VM,又快又省内存。

ActivityThread · 管家

每个 App 进程的主线程管家。它的 main() 是 App 进程的入口:启动 Looper 消息循环,之后 ATMS 的每道指令都变成消息,由它在主线程执行成生命周期回调。注意:它不是一个线程

"
定义
Activity 启动 = 一次由 system_server 居中调度的跨进程接力:发起方经 Binder 提交请求,ATMS 校验并安排任务栈,必要时让 Zygote fork 出目标进程,最后驱动目标进程主线程创建实例、走生命周期直到首帧可见。
— 02

第一程:发起端,从一行代码到 Binder 出口

在你的进程里,这一程很短:层层转手,最后把请求交给 ATMS 的 Binder 代理扔出进程。值得认识的只有一个中间人——Instrumentation。

发起进程内的调用链 · 从你的一行代码到出口
Activity.startActivity() startActivityForResult() Instrumentation.execStartActivity() ATMS 代理 .startActivity() Binder ⇒ system_server

前两步只是重载转发;真正干活的是 Instrumentation,最后一步拿到 ATMS 在本进程的代理对象,发起跨进程调用——请求就此离开你的进程。

Instrumentation · 仪表台

启动与生命周期的必经关卡
概念是什么

每个 App 进程里有唯一一个 Instrumentation 实例,所有 Activity 的启动和每个生命周期回调都从它手上过一遍。平时它只是透明转发;做自动化测试时,测试框架就是把它换掉,才能「监控/拦截」整个 App——这也是它名字(仪表)的由来。

你熟悉的报错 Have you declared this activity in your AndroidManifest.xml? 就是它在 checkStartActivityResult() 里抛的:ATMS 返回「查无此人」,它负责翻译成人话。

框架源码(简化)
Instrumentation.javaJava
public ActivityResult execStartActivity(...) {
    // 拿到 ATMS 在本进程的 Binder 代理,跨进程调用
    int result = ActivityTaskManager.getService()
            .startActivity(whoThread, intent, ...);
    // 启动失败 → 在这里抛出你熟悉的那些异常
    checkStartActivityResult(result, intent);
}
看清本质ActivityTaskManager.getService() 返回的不是 ATMS 本尊,而是它在你进程里的 Binder 代理——对代理调方法,参数被打包送到 system_server,在那边的 ATMS 真身上执行。「像函数调用的 RPC」,这就是 Binder。
— 03

第二程:ATMS 当裁判,Zygote 生进程

请求进了 system_server,ATMS 开始真正的调度。它自己不执行任何生命周期——它只做决定,然后向各 App 进程发号施令

① 验票与找人

拿 intent 去问 PackageManagerService:目标到底是谁?再查权限、exported、启动限制。没在 Manifest 注册的,这一步直接打回。

② 排座次

launchMode 和 Task 栈现状决定:复用已有实例,还是压一个新的进栈?复用的话走 onNewIntent(),后面的创建流程就省了。

③ 保进程

确认目标进程活着:活着就直接下发指令(热/温启动);不在,才去找 Zygote fork(冷启动)。「每次启动都开新进程」是误解。

顺序陷阱 · 在新 Activity 创建之前,ATMS 会先命令当前台上的 Activity 执行 onPause(),并且等它执行完才继续。所以 onPause 里做重活 = 直接拖慢下一个页面的启动——这是「onPause 要轻」这条军规的真正出处。
冷启动支线:Zygote fork 与新进程的第一行代码 cold start

目标进程不存在时,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,冷启动优化第一刀也总是砍向这里的初始化。

框架源码(简化)
ActivityThread.javaJava
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 启动 B)
A.onPause() B.onCreate() B.onStart() B.onResume() B 首帧上屏 A.onStop()

两头是重点:A 的 onPause 挡在 B 的最前面(见上面的顺序陷阱);A 的 onStop 要等 B 画完首帧才执行——系统保证屏幕上始终有东西可看。

— 04

第三程:回到目标进程,实例出生

ATMS 打包一个 ClientTransaction(「去启动这个 Activity」)发给目标进程。指令先落在 Binder 线程上,但生命周期必须在主线程跑——于是有了那次著名的「切线程」。

目标进程内部:从 Binder 线程到首帧

Binder thread → H → main thread
启动事务先由 Binder 线程接收,再通过 Handler 切换到主线程;主线程依次反射创建 Activity、执行 onCreate、onStart、onResume,最后由 ViewRootImpl 完成首帧绘制。 Binder 线程收到事务 ClientTransaction mH.sendMessage() · 切回主线程 ↓ 以下全部发生在主线程 performLaunchActivity 反射 new + attach onCreate() setContentView 只建树 onStart() onResume() addView → ViewRootImpl measure/layout/draw 首帧 生命周期回调全部占着主线程 —— 这就是「回调里不能做耗时操作」的原因。

performLaunchActivity · 实例的出生地

你的 Activity 在这里被 new 出来
概念是什么

三件事:反射创建实例(所以 Activity 不能写有参构造,你也永远不该自己 new 它)、attach(配上 Context 和 PhoneWindow,Activity 从「裸对象」变成「有窗户的页面」)、最后经 Instrumentation 回调 onCreate()——你写的代码到这一刻才登场。

注意 setContentView() 只是把 XML 解析成 View 树挂到窗口上,并不渲染。真正上屏要等 onResume 之后 WindowManager.addView() 把 ViewRootImpl 接进来,走完 measure / layout / draw 才有第一帧

框架源码(简化)
ActivityThread.javaJava
// ① 反射创建 —— 不是你 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);
看源码对不上号?先对版本 · Android 9 起用 ClientTransaction 封装启动指令,之前是 H.LAUNCH_ACTIVITY 消息;Android 10 起 Activity 调度从 AMS 拆出成 ATMS。网上博客新旧混杂,类名对不上时先看它写的是哪个版本——骨架流程从未变过
— 05

易混点与上手

几个最容易栽的认知坑,一张 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 单线程收发更安全。
launchMode 速查:ATMS“排座次”的规则
维度standardsingleTopsingleTasksingleInstance
何时复用从不,每次都新建目标已在栈顶才复用栈内已有就复用,并清掉它上面的全系统仅一个,独占一个栈
复用时的回调—(总走 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——全景图就活生生躺在那条线上。

一句话背走发起方喊一嗓子(Binder),ATMS 排好座次(任务栈),Zygote 生个娃(fork),主线程接力干活(Handler + 反射),生命周期回调只是流程走到哪一步的回执