文章内容
本文内容整理自享学团队直播课《App 启动流程源码分析(三)》
一、从 WMS 方向看启动,问题落在哪里
冷启动的耗时账单最后要落到"首帧什么时候上屏",这条线的终点在 WMS 一侧。但往回追一步就会撞上一个不太好回答的地方:进程创建完成、ApplicationThread 回连 AMS 之后,AMS 在同一段加锁逻辑里干了两件事——把 Application 的信息发过去,以及把 Activity 的启动事务发过去。这两个动作走的都是 IApplicationThread 上的 oneway 方法,AMS 发完就返回,不等任何一边执行完。
那么问题来了:既然两个调用都不等结果,AMS 凭什么敢假设 App 进程里 Application.onCreate 一定跑在入口 Activity 的 onCreate 前面?如果顺序反了,getApplicationContext() 会拿到 null,所有把初始化写在 Application 里的 SDK 都会崩。可现实里这个顺序从来没有翻过车。
结论先摆出来:AMS 在 system_server 里通过 Binder oneway(异步)向 App 进程先后发出 bindApplication 与 scheduleLaunchActivity,这两个动作本身不等待;真正保证先后顺序的是 App 进程主线程的 Handler 消息队列——MessageQueue 按时间戳 FIFO,先入队的先执行;同时 AOSP 在 LoadedApk.makeApplication 里用"Application 已创建则直接返回、否则才创建"做兜底。两条机制叠在一起,Application 必先于 Activity。
Android 10 引入 ClientTransaction 之后,scheduleLaunchActivity 这个方法本身在新版本里已经被 scheduleTransaction + LaunchActivityItem 取代,Android 10/11 又把大量 Activity 逻辑从 AMS 搬到了 ATMS 和 RootWindowContainer。
二、全局概览:谁在哪个进程负责什么

system_server 进程这一侧:
AMS(ActivityManagerService):进程、组件的总调度。App 进程创建完成后回连的入口attachApplication就在它身上。ActivityStackSupervisor:栈与任务的管理者,startSpecificActivity/realStartActivityLocked这两个关键方法在这里。ActivityStarter/ActivityStarterController:解析 Intent、决定 launchMode、决定复用哪个栈哪个任务,是"要不要启动、怎么启动"的决策层。RootWindowContainer:Android 10 之后窗口容器树的根,realStartActivityLocked的部分职责迁到了这里。
App 进程这一侧:
ActivityThread:主线程的载体。main()是进程 Java 层的入口,里面持有一个 Handler 子类H。ApplicationThread:ActivityThread的内部类,是IApplicationThread的服务端实现。它跑在 Binder 线程池分出来的线程上,不是主线程。LoadedApk:一个已加载 APK 的运行期描述,mApplication这个字段就挂在它身上。Instrumentation:负责用 ClassLoader 反射实例化 Application 和 Activity,以及回调生命周期。
两种通信:跨进程的那一段全是 Binder,而且 AMS 发往 App 的这几个调用是 oneway 代理调用;进入 App 进程之后是 Handler 消息队列,主线程按队列顺序一条条执行。顺序保证不在跨进程那一段产生,它在进程内那一段产生——这就是本文要跟到底的路径。
三、启动被拆成三个阶段

从耗时归因的角度,冷启动可以切成三段,切法的依据是"这段谁能改":
① 系统创建进程。AMS 判断目标进程不存在,通过 Socket 请求 Zygote fork,新进程加载 Binder 驱动、跑 ActivityThread.main、回连 AMS。这一段基本不在 App 的可控范围内——App 代码一行都还没执行。做启动优化时它是常数项,只能通过减少进程数、避免多进程互相唤醒来间接影响。
② Application 阶段。bindApplication 到达主线程,创建 Context、创建 Application、跑 attachBaseContext 和 onCreate,中间还夹着 ContentProvider 的安装。这一段完全是 App 自己的代码,是优化的主战场之一。
③ Activity 阶段。入口 Activity 被实例化、onCreate / onStart / onResume、setContentView、measure/layout/draw,直到首帧提交给 WMS 和 SurfaceFlinger。

主链完整展开是这样一条线:
Launcher.startActivity → AMS / ATMS.startActivity → ActivityStarter / ActivityStarterController 解析与决策 → ActivityStackSupervisor.startSpecificActivity → realStartActivityLocked(进程不存在则先 startProcessAsync 通知 Zygote) → [进程创建完成] AMS.attachApplicationLocked → thread.bindApplication(oneway) → 继续 realStartActivity,封装 ClientTransaction → scheduleTransaction / IApplicationThread.scheduleLaunchActivity(oneway) → handleLaunchActivity → performLaunchActivity → LoadedApk.makeApplication(mApplication == null 才创建) → Instrumentation.newActivity + callActivityOnCreate注意这条链上"进程创建完成"这个节点:startSpecificActivity 发现进程不存在时并不会原地卡住等 fork,它把 Activity 的启动请求挂起,等进程回连之后再从 attachApplicationLocked 里续上。阶段②和阶段③的两条指令,就是在这个续上的位置被先后发出去的。
四、一个关键问题:AMS 启动 Application 是同步还是异步?
先看 App 进程向 system_server 暴露的接口长什么样,因为同步还是异步在 AIDL 声明这一层就已经确定了。
// IApplicationThread 是 App 进程向 system_server 暴露的 Binder 服务端接口// 服务端实现在 ActivityThread 内部的 ApplicationThread;AMS 通过 ApplicationThreadProxy 持有其代理public interface IApplicationThread extends IInterface { // oneway 关键字:客户端(AMS)发起调用后立刻返回,不等待 App 进程执行结果 // 待核实:bindApplication 的参数列表随 Android 版本变化,这里用省略号表示未展开 oneway void bindApplication(String packageName, ApplicationInfo info, List<ProviderInfo> providers, ..., int processState, ...); // scheduleLaunchActivity 同样是 oneway,触发 App 进程主线程去真正启动 Activity oneway void scheduleLaunchActivity(Intent intent, IBinder token, ...); // 接口定义结束}输入是 AMS 侧准备好的 ApplicationInfo、Provider 列表、Intent 和 token;输出是 void——oneway 方法不允许有返回值,这是 AIDL 的语法约束,也正是"不等结果"的直接证据。状态变化在 AMS 这一侧只有一个:调用栈立刻弹出,AMS 的锁可以继续往下走。这段代码证明的结论是:AMS 发起 Application 创建是异步的,它没有任何机会在方法返回值上得知 Application 是否已经建好。
接着看这两个调用是在哪里发出的。
// attachApplicationLocked:App 进程创建完成后通过 Binder 回连 AMS,通知"进程已就绪"// thread 是 App 进程暴露的 ApplicationThread 服务端纽带,AMS 之后靠它向 App 进程回发指令private boolean attachApplicationLocked(IApplicationThread thread, int pid, ...) { // mProcessesOnHold:因条件不满足而被暂时挂起的进程/启动请求集合 if (mProcessesOnHold != null && mProcessesOnHold.contains(pid)) { // 目标进程已就绪,从挂起集合移除,允许其启动流程继续 mProcessesOnHold.remove(pid); // 挂起集合处理结束 } // app.makeActive:把该进程状态从"正在启动"切换为"已激活/运行中" app.makeActive(false); // thread.bindApplication:AMS 通过它把 Application 相关信息一次性发给 App 进程 // 这是 oneway 调用,AMS 发出后不等待 App 进程真正构建出 Application thread.bindApplication(...); // 待核实:mProcessesOnHold / makeActive 的容器类型与调用顺序随版本略有不同 return true; // attachApplicationLocked 方法体结束}输入是新进程 Binder 回连时带过来的 thread 和 pid;输出是一个布尔,表示 attach 是否被 AMS 接受。状态变化有三处:挂起集合里去掉该 pid,ProcessRecord 被标记为 active,然后一条 bindApplication 事务被投递出去。方法名带 Locked 后缀,说明它在 AMS 的全局锁内执行——这一点等下讨论顺序时会用到。
这段代码证明的是:bindApplication 的发出点,和后续 Activity 启动的续接点,处在同一个持锁的执行流里。mProcessesOnHold.remove(pid) 之后被放行的正是那些等着这个进程的 Activity 启动请求,它们会在同一段逻辑里继续走到 realStartActivityLocked。也就是说,阶段②和阶段③的两条指令由同一个线程按顺序发出,中间不会被别的线程插进来重排。
那么,bindApplication 到了 App 进程之后,是谁在执行?
// ApplicationThread:IApplicationThread 的服务端实现,运行在 Binder 线程池分配出的一个 Binder 线程上// 它并不直接创建 Application,而是把任务转投给主线程的 Handler Hpublic void bindApplication(...) { // new AppBindData:把跨进程带过来的 Application 信息打包 AppBindData data = new AppBindData(); // 记录 App 的 ApplicationInfo 到待投递数据 data.appInfo = info; // 记录进程状态到待投递数据 data.processState = processState; // sendMessage:投递 BIND_APPLICATION 消息到主线程消息队列,由 H 在主线程执行 // Application 的创建发生在主线程 BIND_APPLICATION 分支,而不是这个 Binder 线程 sendMessage(H.BIND_APPLICATION, data); // 方法体结束(ApplicationThread 只负责投递,不创建 Application)}输入是跨进程带过来的 ApplicationInfo、processState 等;输出还是 void。状态变化是主线程 MessageQueue 里多了一条 BIND_APPLICATION 消息。这段代码证明的结论最关键:Binder 线程不创建 Application,它只是一个投递员。真正的创建被搬到了主线程。这一步是整个顺序保证能成立的前提——两个跨进程指令一旦都被折进同一条主线程队列,它们的先后就不再取决于 Binder 驱动或线程调度,而取决于入队时刻。
五、跨进程异步如何实现:Binder oneway 与内存映射的区别

oneway 在 Binder 这一层具体做了什么,值得和另一个常被混起来的机制分开说。
事实层面:普通 Binder 调用里,客户端线程写入 BC_TRANSACTION 后进入内核等待,直到服务端返回 BC_REPLY、驱动回送 BR_REPLY 才被唤醒——调用方线程在这段时间是挂起的。oneway 声明会让驱动在事务标志位上带 TF_ONE_WAY,驱动收下事务、把它挂进目标进程的待办队列之后立刻返回,客户端线程不进入等待。所以 AMS 的那个 Binder 事务线程发完 bindApplication 就能继续往下执行。
需要和 mmap 区分开:Binder 的"一次拷贝"来自接收进程与内核共享的那块 mmap 映射区,它解决的是数据搬运次数的问题;oneway 解决的是调用方等不等的问题。两者都在 Binder 里,但回答的不是同一个问题。一次拷贝并不会让调用变成异步,同步调用同样只拷贝一次;反过来,oneway 也不会改变数据传递的拷贝次数。把"Binder 快"归因成"所以顺序有保证"是接不上的——快慢和顺序是两回事。
推断层面:同一个客户端向同一个目标进程连续发出的多个 oneway 事务,在驱动里是按到达顺序挂进目标的 async 待办队列的,因此到达 App 进程的顺序与发出顺序一致。这一点符合驱动对 async 事务的队列语义,但本文不把它当作顺序保证的唯一依据——即便这一层出现乱序,后面 makeApplication 的兜底仍然成立。
一个容易误判的地方:oneway 意味着 AMS 无法通过返回值感知失败。所以 App 进程在这条链上出问题时,system_server 看到的往往不是异常,而是超时——ANR 的 Activity launch timeout、Process ... failed to complete startup 这类。做启动问题排查时,日志里没有跨进程异常栈是正常现象,不代表调用成功。
六、两个异步动作的先后怎么保证:Handler 主线程消息队列

回到核心问题。两条指令都异步,都不等结果,顺序从哪来?
答案在 MessageQueue。Handler.sendMessage 最终会走到 enqueueMessage,把消息的 when 设成 SystemClock.uptimeMillis()(无延时设置),队列按 when 从小到大排列,Looper.loop() 从头依次取。同一个 Handler、同一个线程、都不带延时的两条消息,先调用 sendMessage 的那条 when 更小或相等,排在前面,也就先被执行。这是队列的定义性质,不依赖任何额外约定。
于是顺序链条可以完整串起来:
- AMS 在
attachApplicationLocked的持锁执行流里,先发bindApplication,后走到realStartActivityLocked发 Activity 事务。发出顺序在 system_server 单线程内确定。 - 两条
oneway事务先后到达 App 进程的 Binder 线程。 - Binder 线程不执行业务,两条都被
sendMessage折进主线程MessageQueue:先BIND_APPLICATION,后LAUNCH_ACTIVITY(新版本是EXECUTE_TRANSACTION)。 - 主线程
Looper按 FIFO 取出,BIND_APPLICATION先执行完整个handleBindApplication(含Application.onCreate),才轮到 Activity 那条。
第 4 步还有一层强约束:主线程是单线程,handleBindApplication 是一个普通的同步方法调用,它没执行完,Looper 根本不会去取下一条消息。这意味着 Application.onCreate 里的耗时会原封不动地累加到入口 Activity 的启动时间上。
再看 Activity 那条指令是怎么被发出的:
// realStartActivityLocked:真正决定"现在开始启动这个 Activity"// 前提:目标进程已创建并 attach;若进程还未就绪会先走 startProcessAsync 通知 Zygote 创建进程void realStartActivityLocked(ActivityRecord r, WindowProcessController proc, ...) { // 把"启动 Activity"封装成一个 ClientTransaction 事务下发到目标进程 // 事务里携带的是 Activity 生命周期条目(LifecycleItem),将来逐个执行 ClientTransaction transaction = ClientTransaction.obtain(...); // 把 Activity 生命周期条目加入事务,随事务一并下发 transaction.addLifecycleItem(...); // 待核实:具体 lifecycle 条目顺序 // 通过 mClientTransactionHandler.scheduleTransaction 把它丢给 App 进程主线程 mClientTransactionHandler.scheduleTransaction(transaction); // realStartActivityLocked 只负责下发事务,不在此处执行}输入是描述这次启动的 ActivityRecord 和目标进程的 WindowProcessController;输出是 void。状态变化是一个 ClientTransaction 被组装并下发。这段代码证明的是:Activity 的启动同样不是一个同步的方法调用,而是一个被打包投递的事务,它和 bindApplication 在传输层面是同一类东西——所以它们才可能落进同一条主线程队列里排队。
Android 9 引入 ClientTransaction 之后,scheduleTransaction 到达 App 进程侧仍然是 ApplicationThread 的一个 oneway 方法,内部同样 sendMessage(H.EXECUTE_TRANSACTION, transaction)。
七、源码解释
// performLaunchActivity:真正执行 Activity 生命周期入口,主线程调用private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { // makeApplication:启动 Activity 前确保 Application 对象已经存在(mApplication == null 才创建) // 这是"Application 一定先于 Activity"的另一道保障 Application app = r.packageInfo.makeApplication(false, mInstrumentation); // newActivity:用类加载器(ClassLoader)反射创建 Activity 实例 Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent); // callActivityOnCreate:触发 Activity.onCreate 生命周期 mInstrumentation.callActivityOnCreate(activity, r.state); // 中间附带的参数(如 theme、Configuration)在此未展开 return activity; // performLaunchActivity 方法体结束}输入是 ActivityClientRecord,里面带着 packageInfo(就是 LoadedApk);输出是构造好的 Activity 实例。状态变化按行读就是顺序本身:先拿到 Application,再造 Activity,最后回调 onCreate。这三行的相对位置是硬编码在方法体里的,与消息队列无关。总结下这段代码:即使 BIND_APPLICATION 因为某种原因没有先跑,Activity 的创建路径上第一件事仍然是去要 Application。

那"去要"具体是什么行为?
// makeApplication:真正创建 Application 对象的地方,用"若已创建则直接返回"做兜底// 不同版本方法名 makeApplication / makeApplicationInner 略有差异public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) { // mApplication 已经存在:说明 Application 之前已被创建,直接返回,不再重复构建 if (mApplication != null) { // Application 已存在,直接返回同一个实例,避免二次创建 return mApplication; // 判空分支结束 } // 走到这说明 Application 还没创建,用 Instrumentation 通过类加载器实例化 Application Application app = instrumentation.newApplication(cl, appClass, appContext); // 缓存起来,后续 attach / onCreate 都围绕这个实例 mApplication = app; // 待核实:附带的 installContentProviders 等调用顺序随版本略有不同 return app; // makeApplication 方法体结束}输入是 Instrumentation 和类加载信息;输出是唯一的 Application 实例。状态变化只在 mApplication 这一个字段上:null 就创建并写入,非 null 就原样返回。这两个分支合起来给出了两条保证——唯一性(同一个 LoadedApk 下 Application 只会被 new 一次,BIND_APPLICATION 先跑过之后 performLaunchActivity 拿到的是同一个对象,不会二次构建,onCreate 也不会重入)和先行性(万一 Application 还没有,就在这里补上,补完才继续往下走到 newActivity)。
两道机制的分工可以这样对照:Handler 队列决定了"正常情况下谁先跑",makeApplication 的判空决定了"任何情况下 Activity 拿到手的 Application 都不是 null"。前者是时序保证,后者是数据保证。核心问题问的是时序,但工程上真正兜住风险的是后者——它不依赖任何关于调度的假设。
顺带解释一个常见困惑:既然 makeApplication 能兜底,为什么还需要 bindApplication 这条独立的消息?因为 handleBindApplication 干的事远不止 new 一个 Application——设置进程名、初始化 Instrumentation、装配 LoadedApk 与 ClassLoader、安装 ContentProvider、调 Application.attachBaseContext 和 onCreate。makeApplication 只是其中一环,它兜的是对象存在性,兜不了整个进程环境的初始化。
八、从启动优化角度看这个结论
把三个阶段和上面的时序结论合起来看,优化的着力点就很清楚了。
阶段①不可优化,别在这里花时间。从 startProcessAsync 到进程回连,中间是 Zygote fork、Binder 驱动打开、ART 初始化,App 侧改不动。能做的只有减少进程数量——多进程 App 每多一个需要冷启的进程,就多付一次这段常数成本;以及避免在启动路径上唤醒别的进程(比如启动时就去 bind 一个跑在 :remote 的 Service)。
Application.onCreate 是一段纯串行的阻塞窗口。第六节那条"主线程单线程、handleBindApplication 没跑完 Looper 不取下一条消息"的约束,翻译成耗时就是:Application.onCreate 里每多 10ms,入口 Activity 的 onCreate 就晚 10ms 开始,首帧就晚 10ms。这里不存在"系统会并行调度一下"的余地。常见做法是把非必需的 SDK 初始化挪出主线程或延后到首帧之后,把真正必须同步完成的(崩溃采集、埋点会话、多进程共享的配置)留下。
ContentProvider 是隐藏在这段窗口里的成本。Provider 的安装发生在 handleBindApplication 内部,位置在 Application.onCreate 之前。用 androidx.startup 或自建 Provider 做初始化的三方库,其代价全部计在阶段②里,而且不会出现在你自己的 Application.onCreate 代码里——排查时容易漏掉。
阶段③的账要单独记。performLaunchActivity 之后才是 setContentView、measure/layout/draw 和首帧提交。阶段②和阶段③在主线程上首尾相接,用一个总时长很难判断该优化哪边,必须分段计时。
总结
AMS 确实是"同时"发起 Application 与 Activity 两件事的——两个调用都是 oneway(单向)且为异步。
保证时序的正确性来自两个地方:ApplicationThread 把两条 Binder 指令都折进了主线程的 MessageQueue,队列按时间戳 FIFO,先入队的 BIND_APPLICATION 先被单线程的 Looper 完整执行完;LoadedApk.makeApplication 又用 mApplication != null 的判空把"Activity 必须持有一个已存在的 Application"变成了代码路径上的硬约束。时序层面靠队列,数据层面靠判空,两者互不依赖。
验证入口。这条链完全可以在真机上量出来,不必只信源码推理:
- 用 Perfetto(或旧一点的 systrace)抓一次冷启动,在主线程轨道上找
bindApplication、activityStart、activityRestart这几个系统 trace 段,看它们的先后与区间长度。 - 在自己代码里给
Application.attachBaseContext、Application.onCreate、入口Activity.onCreate各打一对Trace.beginSection/endSection,就能把阶段②和阶段③在同一条主线程轨道上切开。 - 首帧的落点看
Choreographer#doFrame的第一帧,以及adb shell am start -W输出的TotalTime/WaitTime。 - 想直接验证顺序结论,在入口 Activity 的
onCreate里打印getApplication()的hashCode,和Application.onCreate里打印的对比——同一个实例,且后者的日志时间戳一定更早。
本文由 AgentWorkFlow 生成