App 启动流程源码分析(二):一条 startActivity 让 App 进程真正活过来

文章内容

本文内容整理自享学团队直播课《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 为准;文中标注"待核实"的地方,我没有在本集视频里拿到足够确认。

二、全局地图:四个进程,三种通信

App 启动的四大进程与三种通信

这张图支持的是"这条链为什么必须跨进程"这个前提。从左到右读四个方框——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 组件结构

这张图支持的是"系统源码是可修改的产品代码"这个判断。图里标出的桌面、应用列表、菜单几个区域,都是 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 的两个分支。所有关于启动优化的讨论,如果分不清自己测的是哪条分支,数据就没有可比性。

java
// 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 的启动被挂起,等进程起来之后再续上。

startSpecificActivity 冷/热启动判断流程

这张图支持的是上面这个"挂起—续上"的结构。按箭头方向读:判断节点向右是 realStartActivityLocked,一步到位;向下是 Socket → Zygote fork 这条长路径,路径末端有一根回到 AMS 的虚线,那根虚线就是被挂起的启动请求重新被拾起的地方。

六、冷启动怎么建进程:Socket 送到 Zygote

startProcessLocked 往下走,最终会到 ZygoteProcess。这一段值得看,是因为它打破了"Android 里跨进程就是 Binder"的惯性认知。

java
// 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 完成之后是自由的。

然后是入口:

java
// 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 进程的第一段自己的代码:

java
// 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 之后:App 进程从 Zygote 副本变成可用的应用进程

这张图支持的是"进程存在"和"进程可用"之间那段距离。竖着读时间轴:fork 返回 pid 是第一格,加载 Binder 驱动是第二格,反射 ActivityThread.main 是第三格,Looper.loop() 是最后一格——只有走到最后一格,这个进程才具备接收 AMS 回调的条件。

八、把 Binder 交给 AMS:attach → attachApplication

main 里那行 thread.attach(false, startSeq) 是整条链的转折点。到这一步为止,通信方向一直是 App 侧被动:AMS 发请求给 Zygote,Zygote fork,进程跑起来。AMS 手里并没有任何能调用这个新进程的手柄。attach 就是补上这个手柄的动作。

java
// 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 这边接住:

java
// 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 又变回冷启动。

App 把 Binder 交给 AMS 到 Application 就绪的调用链

这张图支持的是"通道建立"到"Application 就绪"这一段的因果顺序。从左往右读:attachApplication 是 App 打给 AMS 的,bindApplication 是 AMS 打回 App 的,箭头方向在中间发生了一次反转;反转之后的三个节点——handleBindApplication、创建 Application、onCreate——全部发生在 App 主线程上。

九、App 侧真正活过来:handleBindApplication

bindApplication 是 AMS 调过来的,落地在 Binder 线程池的某个线程上。但 Application 的创建必须在主线程,所以这里要转一次:

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

想自己验证,两个断点就够:

  1. 在 ActivityStackSupervisor.startSpecificActivity 打断点,观察同一个 App 首次启动和退到后台再进入时,app.thread 分别是什么值——冷热启动的分叉直接可见。
  2. 在 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 生成

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

分类:Android系统

评论 (0)

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

加载评论中…

App 启动流程源码分析(二):一条 startActivity 让 App 进程真正活过来 · Yinchen 的博客