Android 系统启动机制(四):Zygote 为什么先预加载,再 fork system_server 和 App?

89 阅读7分钟

第三篇已经把 app_process 追到了 ZygoteInit.main()。此时 ART 已经建立,Framework JNI 也已注册。接下来最值得问的不是“又调用了哪个函数”,而是:

既然一个 Java 进程已经可以运行,为什么不让 system_server 和每个 App 都照这条路径从零启动?

答案藏在 Zygote 的进程模型里:先建立一份适合复用的公共运行环境,再 fork 出子进程;子进程继承的是一份起点,不是最终身份,所以还必须 specialize。

本文只追四件事:

从零启动为什么重复;
preload 建立了什么;
fork / COW 复用了什么;
specialize 为什么不可省略。

线程协调、文件描述符治理、allocator 处理和 USAP 放到文末“深入阅读”,不与主线争夺篇幅。

源码基线: Android 16 QPR2 android-16.0.0_r4frameworks/base commit 45034f0663f960d9ee5fb0a101a4732b71f6e2f4

一、如果每个进程都从零开始,重复的是什么?

先假设没有 Zygote。每创建一个 Android Java 进程,都要重新走一遍:

exec 新进程
  → 装载 app_process 与依赖库
  → 创建 ART Runtime
  → 注册 Framework JNI
  → 加载常用 Framework 类和资源
  → 进入目标 Java 入口

这些步骤并非毫无必要,但其中有相当一部分对 system_server 和大量 App 是共同的。让每个进程独立重复,仍要为每个进程创建 Runtime、完成类和资源初始化,并产生大量进程私有状态。Zygote 把适合继承的公共初始化前移,并让未修改页面更容易继续复用。

这里不能反推“独立 exec 的进程连只读代码页和文件映射也必须各复制一份”:它们本来就可能通过 page cache 共享。Zygote 的额外价值,是把可继承的运行时初始化结果也纳入 fork 起点,而不是发明文件页共享机制。

Zygote 把顺序改成:

一次建立公共运行环境
        ↓
      preload
        ↓
  fork 地址空间快照
        ↓
按目标身份 specialize
        ↓
SystemServer.main() / ActivityThread.main()

它没有取消子进程初始化,而是把“公共、稳定、适合继承”的部分前移一次完成,把“身份相关、进程私有”的部分留到 fork 之后。

二、preload 建立的是公共起点,不是完整 Android

入口在 ZygoteInit.preload()。Android 16 的主流程可以归为几类:

类别典型入口建立的公共起点
常用 Framework 类preloadClasses()提前完成一批类的加载与链接
Framework 资源preloadResources()准备常用 drawable、color state list 等资源
核心共享库preloadSharedLibraries()提前装载多个 Java 进程都会使用的库
文本、JCA 等运行时资源preloadTextResources()warmUpJcaProviders()降低相关能力首次使用的冷启动成本
WebView 等条件项条件式准备入口只在构建和配置允许时建立公共部分
App 进程侧图形/HAL 预热钩子nativePreloadAppProcessHALs()为后续 App 进程准备可继承的客户端侧状态

最后一行尤其容易被过度解释:源码出现 nativePreloadAppProcessHALs(),不等于 Zygote 已经“初始化 Android 的所有 HAL 服务”。它只能证明固定实现调用了面向 App 进程的预加载钩子;HAL 服务是否存在、由谁启动、是否 ready,仍属于独立进程和设备实现的证据范围。

同样不能说“Zygote 预加载了整个 Framework”。真实选择始终有取舍:

加载过少:子进程仍重复大量冷启动;
加载过多:Zygote 变重,许多页面可能永远用不到;
提前建立带线程、锁或外部连接的状态:fork 后可能无法安全继承。

所以 preload 的准确含义是:建立一份经过选择的公共起点。

三、fork / COW 复用的是页面,不是一个可互改的 Java 堆

05_zygote_cow.png

Linux fork() 让子进程获得父进程地址空间的快照。父子已经拥有不同 PID、调度状态和权限,但很多虚拟页最初仍可映射到同一物理页。只有一方写入私有页时,内核才为它复制页面:

fork 刚完成:
父虚拟页 P ─┐
             ├──→ 物理页 M
子虚拟页 P ─┘
​
子进程写入 P:
父虚拟页 P ─────→ 物理页 M
子虚拟页 P ─────→ 新物理页 M'

这就是 Copy-on-Write。它带来两类收益:

  • 减少对公共类、资源和运行时状态的重复装载、解析与初始化;
  • 未修改的私有页可以暂时复用,代码页和文件映射还可能通过 page cache 等机制继续跨进程共享。

但 COW 有清晰边界:

  • 父子不是共享一个可同步修改的 Java 堆;
  • 对象字段、allocator 元数据、GC 和类初始化都可能把页面变为 private dirty;
  • “来自 Zygote”不能推出固定的内存节省比例;
  • 文件映射的共享与匿名私有页的 COW 也不是同一种机制。

因此,更准确的说法不是“App 共享 Zygote 内存”,而是:

App 从 Zygote 的地址空间快照出发,未修改页面可以继续复用,随后随着写入和私有状态增长逐步分离。

四、为什么 fork 之后还必须 specialize?

fork 刚返回的子进程仍然太像 Zygote。它继承了运行时快照、打开资源和父进程的许多属性,却还没有成为某个具体的 system_server 或 App。

Native SpecializeCommon() 会把这份模板副本约束成目标进程。主线可以压成四组变化:

specialize 维度要解决的问题
UID、GID、supplementary groups、capabilities进程最终以谁的身份运行
SELinux、seccomp、mount/storage/data isolation进程能访问什么、能执行什么系统调用
process name、runtime flags、target SDK 等进程进入哪种 Android 运行模式
fork 后运行时与文件描述符修复清除模板身份,保留目标真正需要的状态

这揭示了 Zygote 创建进程的完整模型:

preload:建立公共起点
fork:产生独立子进程并继承地址空间快照
specialize:把模板副本塑造成具体身份
进入 Java main:开始承担 system_server 或 App 职责

只讲 preload,会漏掉进程是怎样出生的;只讲 fork,会漏掉 Android 的权限与隔离边界;只看到 Java main(),又会误以为前面的进程身份已经由 Java 入口创建。

五、system_server 与普通 App 共享模型,但不共享出生路径

两者都从 Zygote 的预加载状态出发,也都要经过 Native fork 和 specialize。不过请求路径不同:

system_server普通 App
主 Zygote 初始化期主动调用 forkSystemServer()运行期通常由 system_server 通过 Zygote socket 提交请求
子进程进入 SystemServer.main()常规子进程进入 ActivityThread.main()
固定系统身份、组和能力集合UID、seInfo、mount、target SDK 等随目标应用决定

这张表只用于划清路径,不在本篇展开 socket 协议。尤其不能倒置成“system_server 通过 socket 请求 Zygote 创建自己”。

深入阅读:主线之外还有哪些 fork 约束?

真正的 Zygote fork 路径还要处理更多工程问题:

  • 线程: 多线程进程 fork 后,子进程只保留调用 fork 的线程;ART/Zygote 必须把 fork 点约束在可预测状态。
  • 文件描述符: 子进程不能意外保留 Zygote server socket、pipe 或越权资源,需要检查、关闭、重开或 detach。
  • allocator 与运行时: fork 前后要协调 GC、daemon 线程、信号和 Native allocator 状态,减少错误锁状态与无意义的 private-dirty。
  • USAP: 可以预先 fork 未特化 App 进程,把 fork 延迟前移;具体请求到达后仍必须 specialize,也不改变 system_server 的直接 fork 路径。

这些内容解释“怎样把 fork 做安全、做快”,但不改变本文的核心模型:公共状态先建立,地址空间快照再复用,目标身份最后落地。

从模板进程到 system_server:还缺哪条证据?

现在已经知道 Zygote 为什么适合作为 Java 进程的公共起点,也知道子进程不能停在 fork 返回的那一刻。

接下来必须把抽象模型落到一条具体分叉:forkSystemServer() 前只有 Zygote;fork 返回后,父分支为什么继续监听,子分支又怎样越过 specialize、关闭 Zygote 资源并进入 SystemServer.main()?这正是下一篇的主线。

源码定位