App 启动流程源码分析(三):启动耗时分析 — WMS 方向

文章内容

本文内容整理自享学团队直播课《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 与 App 进程的职责与两种通信

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 消息队列,主线程按队列顺序一条条执行。顺序保证不在跨进程那一段产生,它在进程内那一段产生——这就是本文要跟到底的路径。

三、启动被拆成三个阶段

冷启动的三阶段划分:系统创建进程、Application、Activity

从耗时归因的角度,冷启动可以切成三段,切法的依据是"这段谁能改":

① 系统创建进程。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。

App 冷启动主流程:system_server 与 App 进程之间的调用链

主链完整展开是这样一条线:

plain text
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 声明这一层就已经确定了。

java
// 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 是否已经建好。

接着看这两个调用是在哪里发出的。

java
// 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 进程之后,是谁在执行?

java
// 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 与内存映射的区别

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 主线程消息队列

主线程 Handler 消息队列 FIFO:Application 先于 Activity

回到核心问题。两条指令都异步,都不等结果,顺序从哪来?

答案在 MessageQueue。Handler.sendMessage 最终会走到 enqueueMessage,把消息的 when 设成 SystemClock.uptimeMillis()(无延时设置),队列按 when 从小到大排列,Looper.loop() 从头依次取。同一个 Handler、同一个线程、都不带延时的两条消息,先调用 sendMessage 的那条 when 更小或相等,排在前面,也就先被执行。这是队列的定义性质,不依赖任何额外约定。

于是顺序链条可以完整串起来:

  1. AMS 在 attachApplicationLocked 的持锁执行流里,先发 bindApplication,后走到 realStartActivityLocked 发 Activity 事务。发出顺序在 system_server 单线程内确定。
  2. 两条 oneway 事务先后到达 App 进程的 Binder 线程。
  3. Binder 线程不执行业务,两条都被 sendMessage 折进主线程 MessageQueue:先 BIND_APPLICATION,后 LAUNCH_ACTIVITY(新版本是 EXECUTE_TRANSACTION)。
  4. 主线程 Looper 按 FIFO 取出,BIND_APPLICATION 先执行完整个 handleBindApplication(含 Application.onCreate),才轮到 Activity 那条。

第 4 步还有一层强约束:主线程是单线程,handleBindApplication 是一个普通的同步方法调用,它没执行完,Looper 根本不会去取下一条消息。这意味着 Application.onCreate 里的耗时会原封不动地累加到入口 Activity 的启动时间上。

再看 Activity 那条指令是怎么被发出的:

java
// 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)。

七、源码解释

java
// 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。

LoadedApk.makeApplication 的判空兜底逻辑

那"去要"具体是什么行为?

java
// 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 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

App 启动流程源码分析(三):启动耗时分析 — WMS 方向 · Yinchen 的博客