文章内容
本文内容整理自享学团队直播课《App 启动流程源码分析(二)》
一、本集要解决的问题
《一》把"启动"的边界定死了:从点击 icon 到主 Activity 显示。也画了四进程的地图,结尾留了一个口子——下一篇从 Launcher 的 startActivity 跟到 AMS 请求 Zygote fork。本集从这个口子接上。
核心问题只有一个:从点击桌面图标,到 App 进程真正跑起来并创建好 Application,AMS、Zygote、App 进程各自做了什么?
结论先摆出来:AMS(ActivityManagerService)先在 startSpecificActivity 里判断目标进程是否已存在,存在就是热启动,不存在就是冷启动;冷启动时 AMS 通过本地 Socket 让 Zygote fork 出新进程;fork 出来的 App 进程加载 Binder 驱动、用反射调起 ActivityThread.main,进程真正启动后立刻把 ApplicationThread 这个 Binder 对象交给 AMS(attachApplication);此后 AMS 反向调用 App 的 bindApplication,App 在主线程的 handleBindApplication 里创建 Context 和 Application 并跑 onCreate。这条链走完,App 进程才算活过来。
关于版本:下文所有源码都是 AOSP(Android Open Source Project)的节选,形态取自 Android 8~12 区间,为讲清主链做了裁剪——参数列表、判空分支、日志和 trace 调用大多被删掉了。不同版本方法签名和字段名可能有出入(比如 attach 的 startSeq 参数在较早版本里并不存在)。涉及具体行号的地方,以你手上那份 AOSP 为准;文中标注"待核实"的地方,我没有在本集视频里拿到足够确认。
二、全局地图:四个进程,三种通信

这张图支持的是"这条链为什么必须跨进程"这个前提。从左到右读四个方框——Launcher 进程、system_server 进程、Zygote 进程、App 进程——再看方框之间连线上的标注,标注的不是函数名,是通信方式。
四个进程的分工在《一》里说过,这里只留和本集有关的边界:
- Launcher 进程:桌面。它持有 icon,也是
startActivity的调用发起方。 - system_server 进程:AMS 住在里面。AMS 不是独立进程,它是 system_server 中的一个服务,跑在 Binder 线程池分出来的事务线程上。
- Zygote 进程:所有 App 进程的父进程,负责 fork。
- App 进程:本集的终点,
ActivityThread、Application都在这里。
三种通信方式对应三段不同的边界:进程间调用走 Binder(Launcher↔AMS、AMS↔App),Zygote 创建进程走 Socket,进程内的任务投递走 Handler/Looper。三者不能互换,后面每一节都会撞到这个区别。
会出场的关键对象:ActivityManagerProxy(客户端侧的 AMS 代理)、ActivityManagerService(服务端)、ApplicationThread(App 侧的 Binder 服务端)与 ApplicationThreadProxy(AMS 侧持有的代理)、ActivityThread、ContextImpl、LoadedApk、Application。
三、为什么要读系统源码:从车机说起

这张图支持的是"系统源码是可修改的产品代码"这个判断。图里标出的桌面、应用列表、菜单几个区域,都是 Car Launcher 的组件,它们和手机 Launcher 是同一套 Android framework 上的实现。
车厂中控屏那块屏幕,运行的是安卓。定制中控就是在改 Android 系统本身或 AP(Application Processor)侧的系统层代码,不是在写一个普通 App。改法的起步点通常落在两处:Launcher 的形态,以及 AMS 这类系统服务的行为。
这决定了读源码的角度:startActivity 在应用开发里是一个 API,在系统开发里是一段可以改的逻辑。冷/热启动的判断、进程创建的时机、Application 的构建顺序,都在 system_server 和 Zygote 的代码里,改得动。
课程本身还带了 AMS 专题和 Binder 专题两份文件列表,属于课程编排的部分,与本集主链无关,此处不展开。
四、起点:Launcher 把"启动 App"托付给 AMS
点击 icon 之后,Launcher 干的事其实很短:拿到 AMS 的代理对象,调用 startActivity,然后就没它什么事了。
它怎么拿到代理?靠 ServiceManager。这里可以借一个类比:ServiceManager 之于系统服务,像 DNS 之于域名——服务端启动时用名字注册自己,客户端用同一个名字查回一个可调用的句柄。类比到此为止,两者的失效边界完全不同:DNS 返回的是地址,查回来还要自己发起连接;ServiceManager 返回的是一个 IBinder,包装成 Proxy 之后调用它的方法就等于跨进程调用了服务端的同名方法,中间没有你可见的连接过程。而且 ServiceManager 本身也是 Binder 通信,它是 handle 固定为 0 的那个特殊节点。
从 Launcher 的视角看,ActivityManagerProxy 和一个本地对象没有区别:调 startActivity,方法返回,参数被序列化送到 system_server,AMS 的 startActivity 被执行。Launcher 是客户端,AMS 是服务端。
AMS 收到请求后,会走一段解析和调度:解析 Intent、检查权限、找到目标 Activity 的 ActivityRecord,再交给 ActivityStarter / ActivityStack / ActivityStackSupervisor 这一组对象处理栈和任务。这一段的细节留到下一篇,本集只需要知道它最终会落到一个方法上。
五、冷/热启动的岔路口:startSpecificActivity
为什么先看这个方法:它是"进程要不要新建"这个决策的唯一落点。冷启动和热启动的差别,在源码层面就是这一个 if 的两个分支。所有关于启动优化的讨论,如果分不清自己测的是哪条分支,数据就没有可比性。
// ActivityStackSupervisor.java:每个 Activity 启动前都要先走到这里,判断该不该新建进程void startSpecificActivity(ActivityRecord r, boolean andResume, boolean checkConfig) { // 取出这个 Activity 所依赖的 App 进程记录 ProcessRecord app = r.app; // 进程记录存在,并且它已经绑定过 ApplicationThread:说明进程早就在跑 if (app != null && app.thread != null) { // 进程现成的,直接在里面启动这个 Activity,这就是热启动 realStartActivityLocked(r, app, andResume, checkConfig); // 分支到此结束,不需要走建进程的逻辑 return; } // 进程不存在:没法直接启动 Activity,只能先请求系统拉起一个新进程,这就是冷启动 mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0, "activity", r.intent.getComponent(), false, false, false, null, null, null, null, null);}输入是一个 ActivityRecord,它描述了"要启动哪个 Activity、属于哪个包、跑在哪个进程名下"。判断条件是两个:app != null 说明 AMS 里有这个进程的记录,app.thread != null 说明这个进程已经把自己的 ApplicationThread 交上来了。两个条件都成立,才能认为进程可用。
这里容易产生一个疑问:为什么不只判断 app != null?因为 ProcessRecord 的创建早于进程真正可用。AMS 在请求 Zygote fork 之前就会先建好这条记录,此时进程还没起来,app.thread 还是 null。thread 这个字段是不是 null,才是"进程活了没有"的真正标志——第八节会看到它是在 attachApplicationLocked 里被赋值的。
两条分支的状态变化不同:热启动分支不改变进程数量,只是往已有进程里加一个 Activity;冷启动分支的输出是一次进程创建请求,startSpecificActivity 会直接返回,这个 Activity 的启动被挂起,等进程起来之后再续上。

这张图支持的是上面这个"挂起—续上"的结构。按箭头方向读:判断节点向右是 realStartActivityLocked,一步到位;向下是 Socket → Zygote fork 这条长路径,路径末端有一根回到 AMS 的虚线,那根虚线就是被挂起的启动请求重新被拾起的地方。
六、冷启动怎么建进程:Socket 送到 Zygote
startProcessLocked 往下走,最终会到 ZygoteProcess。这一段值得看,是因为它打破了"Android 里跨进程就是 Binder"的惯性认知。
// ZygoteProcess.java:AMS 侧通过本地 Socket 把"新建一个进程"的请求发给 Zygotepublic Process.ProcessStartResult startViaZygote(String[] args, int uid, int gid, ...) { // 打开(或复用)到 Zygote 的 Unix 本地 Socket 连接,注意这里不是 Binder ZygoteState state = openZygoteSocketIfNeeded(abi); // 把启动参数写进 socket,并阻塞等待 Zygote 返回新进程的 pid return zygoteSendArgsAndGetResult(state, args);} // ZygoteServer.java:Zygote 扮演的是服务器角色,主循环一直在等请求void runSelectLoop(String abiList) { // 死循环:作为服务器,Zygote 要在整个开机周期内持续运行 while (true) { // 阻塞等待客户端(AMS 侧)写过来的启动请求,拿到一条连接 ZygoteConnection connection = acceptCommandPeer(...); // 分发这条命令,内部会真正执行 fork connection.processCommand(this); }} // ZygoteConnection.java:处理一条启动命令,真正执行 forkprivate Runnable processCommand(...) { // 复制当前 Zygote 进程,得到一个新的 App 进程 pid = Zygote.forkAndSpecialize(uid, gid, gids, rlimits, ...); // fork 出来的子进程从这里继续往下走初始化流程 return handleChildProc(...);}三段代码串起来是一条请求-应答:AMS 是客户端,写参数;Zygote 是服务端,runSelectLoop 收,processCommand 执行 forkAndSpecialize。输出是一个 pid,返回给 AMS 记进 ProcessRecord。此刻新进程已经存在于内核里,但它还没有执行任何 App 的代码。
为什么用 Socket 而不是 Binder? 和 fork 的语义有关:fork 只复制调用它的那一个线程,进程里其他线程持有的锁在子进程中会变成永远没人释放的状态。Binder 通信要靠线程池,一旦 Zygote 接入 Binder,它就是个多线程进程,fork 出来的子进程极易死锁或者状态错乱。用 Socket 可以让 Zygote 在 fork 时刻保持单线程姿态。这一段属于 Binder 专题的内容,本集点到为止。
七、fork 之后:加载 Binder 驱动,反射调起 main
新进程已经存在,但它现在还是 Zygote 的一个副本。让它变成"某个 App 的进程",靠的是 fork 之后这几步。
先是 Binder 驱动:子进程需要打开 /dev/binder、映射内存、起 Binder 线程池,之后它才具备被 AMS 反向调用的能力。这一步和上一节 Zygote 自己不碰 Binder 并不矛盾——不碰 Binder 的是父进程 Zygote,子进程 fork 完成之后是自由的。
然后是入口:
// ZygoteInit.java:fork 出来后,子进程从这里继续运行private static Runnable childZygoteInit(int targetSdkVersion, String[] argv, ...) { // 继续做子进程层面的初始化,把参数交给 RuntimeInit return RuntimeInit.applicationInit(targetSdkVersion, argv, ...);} // RuntimeInit.java:反射调用目标类的 main,App 进程由这里进入 ActivityThread.mainprivate static Runnable applicationInit(int targetSdkVersion, String[] argv, ...) { // 按类名反射拿到 ActivityThread 这个类 Class<?> clazz = Class.forName("android.app.ActivityThread"); // 取出它的 main 方法 Method main = clazz.getMethod("main", String[].class); // 执行 main,App 进程从这里进入真正的启动逻辑 main.invoke(null, new Object[]{argv}); // 返回值用于把后续执行交还给调用方 return null;}反射拿到的是 ActivityThread.main,不是我们自己写的 Activity,也不是 Application。这是一个容易错位的地方:App 进程的入口方法和 Java 程序一样是 main,只不过这个 main 属于 framework 的类,我们写的所有代码都在它之后被拉起来。
用反射而不是直接 new,原因在于 Zygote 侧的代码不该编译期依赖具体要启动的类——同一个 applicationInit 也用于启动别的入口(比如 system_server 走的是 SystemServer),类名是从参数里传进来的字符串。
ActivityThread.main 是 App 进程的第一段自己的代码:
// ActivityThread.java:App 进程真正的入口,由上面的反射调用进入public static void main(String[] args) { // 打一个 trace 标记,方便在 systrace / Perfetto 里定位启动阶段 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain"); // 为当前线程创建主 Looper,App 进程从此拥有消息循环 Looper.prepareMainLooper(); // 构造 ActivityThread 实例,它内部持有 ApplicationThread 与主线程 Handler ActivityThread thread = new ActivityThread(); // attach:把 ApplicationThread(App 端的 Binder)注册给 AMS thread.attach(false, startSeq); // 主线程进入阻塞式消息循环,App 进程靠它一直活着 Looper.loop();}这段代码回答了一个常被问到的问题:主线程的 Looper 是谁准备的。Looper.prepareMainLooper() 就在这里,早于任何 Application 和 Activity 的代码。Looper.loop() 之后 main 不会返回,主线程从此只做一件事——从消息队列里取消息并分发。后面所有生命周期回调,都是以消息的形式投进这个循环的。

这张图支持的是"进程存在"和"进程可用"之间那段距离。竖着读时间轴:fork 返回 pid 是第一格,加载 Binder 驱动是第二格,反射 ActivityThread.main 是第三格,Looper.loop() 是最后一格——只有走到最后一格,这个进程才具备接收 AMS 回调的条件。
八、把 Binder 交给 AMS:attach → attachApplication
main 里那行 thread.attach(false, startSeq) 是整条链的转折点。到这一步为止,通信方向一直是 App 侧被动:AMS 发请求给 Zygote,Zygote fork,进程跑起来。AMS 手里并没有任何能调用这个新进程的手柄。attach 就是补上这个手柄的动作。
// ActivityThread.java:App 一启动就把自己的 Binder 交给 AMSprivate void attach(boolean system, long startSeq) { // 通过 ServiceManager 拿到 AMS 的客户端代理 final IActivityManager mgr = ActivityManager.getService(); // mAppThread 是 ApplicationThread,一个 Binder 对象——把它交给 AMS 持有 try { mgr.attachApplication(mAppThread, startSeq); } // 跨进程调用可能抛远程异常,这里按 framework 的惯例吞掉 catch (RemoteException ex) { /* 忽略 */ }}方向要看清楚:这一次调用是 App → AMS,App 是客户端。传出去的参数 mAppThread 是 App 进程里的 Binder 服务端对象,AMS 收到的是它的代理(ApplicationThreadProxy)。一次调用之后,两个方向的通道就都通了。
AMS 这边接住:
// ActivityManagerService.java:App 进程把 Binder 交过来,AMS 收下public final void attachApplication(IApplicationThread thread, long startSeq) { // 全局状态需要串行修改,先加锁 synchronized (this) { attachApplicationLocked(thread, ...); }}private final boolean attachApplicationLocked(IApplicationThread thread, int pid, int uid, long startSeq) { // 按 pid 找到之前建进程时创建的那条进程记录 ProcessRecord app = getProcessRecordLocked(pid, ...); // AMS 持有 App 的 ApplicationThread——从这一刻起 AMS 能反向调用 App app.thread = thread; // 反过来通知 App 去绑定 Application thread.bindApplication(...); // 返回绑定是否成功 return true;}app.thread = thread 这一行值得单独指出来:它就是第五节 startSpecificActivity 里判断的那个字段。冷启动这条长路径绕了一圈,最后改变的状态就是它。改完之后再回头调 startSpecificActivity,if 会走进热启动那一侧。
类比一下:像客户端主动把自己的回调地址告诉服务端。类比的失效边界在于,这里交出去的不是一个地址,而是一个内核维护引用计数的 Binder 节点——App 进程一旦死掉,AMS 侧的代理会收到 death notification,app.thread 随之被清空,下一次启动这个 App 又变回冷启动。

这张图支持的是"通道建立"到"Application 就绪"这一段的因果顺序。从左往右读:attachApplication 是 App 打给 AMS 的,bindApplication 是 AMS 打回 App 的,箭头方向在中间发生了一次反转;反转之后的三个节点——handleBindApplication、创建 Application、onCreate——全部发生在 App 主线程上。
九、App 侧真正活过来:handleBindApplication
bindApplication 是 AMS 调过来的,落地在 Binder 线程池的某个线程上。但 Application 的创建必须在主线程,所以这里要转一次:
// ActivityThread.java:bindApplication 由 Binder 线程执行,用 Handler 投递到主线程public final void bindApplication(String processName, ApplicationInfo appInfo, ...) { // 把 AMS 传过来的一堆参数打包 AppBindData data = new AppBindData(); // 通过主线程 Handler 把绑定任务投递出去,等主线程 Looper 取到它 sendMessage(H.BIND_APPLICATION, data);}private void handleBindApplication(AppBindData data) { // 创建 App 进程的全局 Context,内部装载包信息、资源、ClassLoader ContextImpl appContext = ContextImpl.createAppContext(this, data.appInfo); // data.info 是 LoadedApk,用它和 ClassLoader 构建出 Application 实例 Application app = data.info.makeApplication(...); // 记录为这个进程的初始 Application mInitialApplication = app; // 通过 Instrumentation 触发 Application 的生命周期回调 onCreate mInstrumentation.callApplicationOnCreate(app);}这里是本集的终点,也是三个常见问题的答案落点。
Application.onCreate 到底在哪被触发? mInstrumentation.callApplicationOnCreate(app) 这一行,运行在主线程,运行在任何 Activity 之前。《一》里提到 Application.onCreate 是各类 SDK 初始化集中的地方,之所以能成为耗时热点,就是因为它卡在主线程消息循环的最前面,后面的 Activity 消息只能排队。
Application 为什么被称为资源与配置的容器? 看 makeApplication 的输入:data.info 是 LoadedApk,它持有 APK 的路径、ApplicationInfo、ClassLoader 和 Resources。Application 被创建时,这些已经就绪并挂在它的 base Context 上。整个进程共享这一份,所以它天然适合放全局的东西。
Application 为什么没有 UI? 因为它不是 Activity,没有 Window,也没有 PhoneWindow/DecorView 那套结构。它的 Context 是 ContextImpl.createAppContext 造出来的应用级 Context,不带 UI 相关的 token,也就无法作为一个可显示窗口的宿主。用它做 Dialog 的 Context 会失败,原因在同一处。
顺带说一句时序:attachBaseContext 早于 onCreate,而 ContentProvider 的安装(installProvider)夹在两者之间。这解释了为什么第三方 SDK 借 ContentProvider 做初始化能"抢在 Application.onCreate 之前"——它们并没有绕过 framework,只是站在了这条顺序更靠前的位置上。
Application 就绪之后,AMS 会回到被挂起的那次 Activity 启动,重新走 startSpecificActivity,这一次进入 realStartActivityLocked,通过 ApplicationThread 把启动事务发给 App 进程,由 TransactionExecutor 在主线程上执行 launchActivity,Activity 的生命周期才开始跑。(TransactionExecutor 这套事务机制是 Android 9 之后的形态,更早的版本走的是 H.LAUNCH_ACTIVITY 消息,具体的分界版本待核实。)
十、总结
回到开头的问题。这条链解决的是一件事:如何在一个还不存在的进程里,启动一个 Activity。
拆开看,三方各自的职责都比较清楚。AMS 负责决策与调度——判断进程在不在、决定要不要建、建好之后把启动请求接回来。Zygote 只负责一件事,复制自己,它不知道也不关心要启动的是哪个 App。App 进程负责把自己"接入"系统:加载 Binder 驱动、起主 Looper、把 ApplicationThread 交出去,然后按 AMS 的指令构建 Context 和 Application。
三方之间的粘合剂是 Binder 的双向性。attachApplication 和 bindApplication 是同一条通道的两个方向,前者让 AMS 认识 App,后者让 AMS 指挥 App。整条冷启动链路里,最关键的状态变化就是 app.thread 从 null 变成非 null。
想自己验证,两个断点就够:
- 在
ActivityStackSupervisor.startSpecificActivity打断点,观察同一个 App 首次启动和退到后台再进入时,app.thread分别是什么值——冷热启动的分叉直接可见。 - 在
ActivityManagerService.attachApplicationLocked的app.thread = thread那行打断点,然后单步跟进thread.bindApplication,看它怎么跨进程落到 App 的 Handler 上。
不方便调 framework 的话,用 logcat 观察顺序也能看出结构:过滤 ActivityManager 与 ActivityThread 的 tag,冷启动一次,attachApplication → bindApplication → 自己在 Application.onCreate 里打的日志,三者的先后关系和上面的链路一致。再配合 systrace/Perfetto 抓同一次冷启动,ActivityThreadMain 这个 trace section 的位置正好标出 main 的起点。
下一篇进入 ActivityStarter 与 ActivityStartController:Application 已经就绪之后,AMS 是怎么把那个被挂起的 Activity 真正启动起来的。
本文由 AgentWorkFlow 生成