Zygote 通过 Socket 创建 App 的源码路径与架构解释

Android 启动新 App 进程时,Zygote 并不直接使用 Binder,而是通过本地 Socket 接收命令并 fork 出子进程。这是 AOSP 源码中可以逐行验证的事实。

从 ProcessList.startProcessLocked 开始,系统会调用 ZygoteProcess.start,进而通过 ZygoteState.connect 建立到 Zygote 的 Socket 连接。整个路径不经过 Binder 服务,而是直接使用 LocalSocket 和 InputStream/OutputStream 传输启动参数。

ProcessList 如何发起 Zygote 请求

在 frameworks/base/services/core/java/com/android/server/am/ProcessList.java 中,startProcessLocked 方法最终会调用 ZygoteProcess 的 start 方法。这里传入的参数包括 uid、gid、进程名、ABI 列表以及各种启动旗标。

ZygoteProcess 维护两个 ZygoteState 实例,分别对应 primary Zygote 和 secondary Zygote。每次启动新进程时,先调用 ZygoteState.connect()。connect 方法会创建 LocalSocket,连接到 “/dev/socket/zygote” 或 “/dev/socket/zygote_secondary”。

这一步完全是 Socket 编程,没有任何 Binder 的痕迹。源码中明确使用 LocalSocketAddress.Namespace.FILESYSTEM,连接路径固定。这属于可直接在 AOSP 中看到的实现事实。

ZygoteState 如何封装 Socket 通信

ZygoteState 类位于 frameworks/base/core/java/com/android/internal/os/ZygoteProcess.java。它持有 LocalSocket 实例,并提供 writeCommand 和 readStatus 方法。

writeCommand 把所有启动参数拼接成字符串列表,第一行是参数个数,后面每行一个参数。Zygote 端通过 readLine 逐行解析。这种文本协议简单直接,避免了 Parcel 的序列化开销。

连接建立后,ZygoteProcess 向 Zygote 发送 “–runtime-args”、"–setuid"、"–setgid" 等参数,最后发送 “–start-child-zygote” 或普通启动命令。整个过程都是纯文本通过 Socket 传输。

Zygote 端如何接收并 fork

Zygote 的入口在 frameworks/base/core/java/com/android/internal/os/ZygoteServer.java。runSelectLoop 方法使用 Selector 监听 Socket 上的连接。

当有新连接到来时,acceptCommandPeer 接受连接,读取命令行参数。然后调用 Zygote.forkAndSpecialize。

forkAndSpecialize 先执行 nativeForkAndSpecialize,这是一个 JNI 调用,最终调用 Linux 的 fork()。子进程返回后,执行 specializeCommon,进行 SELinux 设置、能力调整、进程名修改等操作。

这些步骤全部在 ZygoteServer 和 Zygote 类中实现,Socket 只是命令传递的通道,真正创建进程靠的是 fork 系统调用。

为何选择 Socket 而非 Binder

源码中没有一行注释明确说明“为什么不用 Binder”。所有“为了启动更快”“避免 Binder 线程池竞争”“Zygote 需要在 fork 前保持单线程”等说法,都属于架构层面的解释,而非直接的源码事实。

从实现看,Zygote 在启动时就处于单线程监听状态。fork 之后,子进程会继承父进程的所有文件描述符,包括 Binder 驱动的文件描述符。如果使用 Binder,子进程需要立刻关闭或重置这些 fd,否则可能出现 Binder 线程池混乱。

而 Socket 方式只需要关闭一个连接描述符,处理更简单。这一点在社区讨论中被反复提及,但 AOSP 源码本身并未直接写出这个理由。

文本协议与性能考量

Zygote 使用的命令协议是纯文本,每行一个参数。这种设计在源码中清晰可见:ZygoteCommandBuffer 和 readArgumentList 方法都是字符串操作。

相比 Binder 的 Parcel 序列化,文本协议解析开销更小,尤其在启动早期阶段。Zygote 本身就是为了快速 fork 而存在,减少任何可能引入延迟的机制都有意义。

但这仍然是基于源码结构的合理推断,而非源码中直接写明的设计文档。

历史演进与当前实现

早期 Android 版本中,Zygote 通信方式已经固定为 Socket。Android 8.0 引入 Project Treble 后,Zygote 的职责被进一步明确,但通信方式没有改变。

在 Android 14 的最新 AOSP 中,ZygoteState、ZygoteServer 的核心逻辑依然保持一致。forkAndSpecialize 的参数列表越来越长,包含了越来越多的启动选项,但传输方式仍是 Socket。

这说明这种机制经过了十几年验证,稳定性优先于理论上的“更现代 IPC 方式”。

常见误解的澄清

很多文章把“Zygote 用 Socket 是为了绕过 Binder 线程池限制”当作确定事实。其实源码里只展示了“它确实用了 Socket”,而“为什么”的部分属于架构解释,需要结合 fork 语义和 Binder 驱动特性来理解。

同样,“Socket 比 Binder 快”也不是绝对的。在启动路径上,Socket 的优势在于实现简单、状态少,而不是理论带宽。Binder 在高并发场景下有更好的调度机制,但 Zygote 的使用场景是低频、关键路径。

区分源码事实和架构解释很重要。源码事实是:ProcessList 通过 LocalSocket 连接 Zygote,发送文本命令,Zygote fork 后 specialize。架构解释是:这样做避免了 fork 后清理 Binder 状态的复杂性。

理解这个区别,才能真正读懂 Zygote 的设计。

(正文字数约 2150 字)

参考来源