文章内容
- 一、Zygote 要回答的三个问题
- 二、全局概览:init → Zygote → 应用进程的边界
- 三、Zygote 创建:appprocess 的 main() 方法
- 四、native → Java:AndroidRuntime::start 启动虚拟机
- 启动虚拟机
- 五、ZygoteInit.main:进入 Java 后的三步
- 六、预加载:把 Java 环境先装好(为 fork 的写时复制铺垫)
- 七、runSelectLoop:Zygote 的孵化循环
- 安全性问题
- 八、ZygoteConnection.runOnce:读参数并 fork 一个应用进程
- forkAndSpecialize 是怎么 fork 的
- 九、forkSystemServer:孵化 systemserver
- 9.1 systemserver 挂了,Zygote 为什么也要跟着死
- 十、进程 fork
- 10.1 虚拟地址: 0-3G 是用户空间
- 10.2 fork:只拷贝 mmstruct,页只读共享
- 10.3 进程fork收益
- 十一、怎么优化:多进程、服务进程内存、预加载并行
- 11.1 多进程:各司其职,别互相拖累
- 11.2 服务进程内存:越小越不容易被杀
- 11.3 系统侧:预加载并行化
- 十二、总结与验证
一、Zygote 要回答的三个问题
在 Android 设备上执行 adb shell ps -A -o USER,PID,PPID,NAME | grep -E 'zygote|system_server',你会看到 zygote 通常只有一个实例,PPID 是 1;而任何一个第三方应用进程的 PPID 都是这个 zygote。再看进程名,zygote 和 system_server 都不是用户空间自己 fork 出来的普通进程。
这三个现象说明了三个问题:
app_process本来是个 native 程序,它凭什么能执行 Java 代码?也就是init.rc里那条service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server被 init 用 fork+execve 拉起之后,这个纯 C++ 可执行文件是怎么跨进 Java 世界的?- Zygote 为什么要一直活着、一直等?它靠什么机制「类进程」一样持续接收请求而不是自己退出?
- 它孵化一个应用进程为什么那么快?为什么不把每个应用都让 init 重新从头启动一遍?
Android 6.0 的答案可以压成一条主链:app_process 的 main() 创建 AndroidRuntime 并调 start("com.android.internal.os.ZygoteInit") → startVm() 启动 Java 虚拟机、startReg() 注册 JNI → CallStaticVoidMethod 进入 ZygoteInit.main() → 注册 zygote socket → preload() 预加载类/资源/so → runSelectLoop() 进入死循环,用 Os.poll 监听 socket,收到命令就 acceptCommandPeer 新建 ZygoteConnection,再 runOnce() 读参数并 forkAndSpecialize 出一个新进程。

应用启动快的原因不是「启动快」,而是「fork 时继承原有进程资源」。
二、全局概览:init → Zygote → 应用进程的边界
先把参与者搞清楚。Zygote 不是一个孤立的进程:左边是 native 世界(C++、AndroidRuntime、JVM 的启动),右边是 Java 世界(ZygoteInit 和它的子进程);
上面是它的父进程 init(PID 1),下面是它 fork 出来的 system_server 和所有应用进程。

这条边界决定了 Zygote 的三种身份:
- ** init 的子进程**、又是绝大多数应用进程(Java 应用与 system_server)的祖先。init 用 fork+execve 把它拉起来,此后它不再退出。(init 自己还会用 fork+execve 直接拉 native 服务,那些并不是 zygote 的后代。)
- ** Java 虚拟机宿主**。
app_process启动时startVm把 ART 装进自己的进程(Android 6.0 已无 Dalvik 可选,DVM 只是历史上的说法),之后每一个 fork 出来的应用进程都直接继承这份已经跑起来的虚拟机实例,不用再启动一遍。 - 守护进程。它靠一个
zygotesocket 接收来自 AMS 的请求,除此之外不再干别的活。
三、Zygote 创建:app_process 的 main() 方法
app_process 的入口 main() 第一件事不是启动虚拟机,而是解析自己的命令行参数,并且判断自己该以哪种模式运行:
// 代码位置 /frameworks/base/cmds/app_process/app_main.cpp 第 186—316 行// 当前应用的可执行文件入口int main(int argc, char* const argv[]){// 禁止子进程获得新的特权位,防止 setuid 提权 if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {// 旧内核会返回 EINVAL,不属于致命错误,忽略 if (errno != EINVAL) {// 其余错误直接终止 LOG_ALWAYS_FATAL("PR_SET_NO_NEW_PRIVS failed: %s", strerror(errno)); return 12; } }// 创建 AppRuntime 子类,参数块从 argv[0] 到最后一个参数结尾 AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));// 忽略 argv[0](argv[0] 是可执行文件自身,不是业务参数) argc--; argv++;// 逐个读参数,遇到非 '-' 开头或 '--' 就停,之前的是给虚拟机的选项 int i; for (i = 0; i < argc; i++) { if (argv[i][0] != '-') { break; } if (argv[i][1] == '-' && argv[i][2] == 0) { ++i; // Skip --. break; } runtime.addOption(strdup(argv[i])); }// 解析业务参数:是否 zygote 模式、是否启动 system_server 等 bool zygote = false; bool startSystemServer = false; bool application = false; String8 niceName; String8 className;// 跳过未使用的「父目录」参数 ++i; while (i < argc) { const char* arg = argv[i++];// 命中 --zygote,进入 zygote 模式并设置进程名 if (strcmp(arg, "--zygote") == 0) { zygote = true; niceName = ZYGOTE_NICE_NAME;// 命中 --start-system-server,则 zygote 起来后要继续启动 system_server } else if (strcmp(arg, "--start-system-server") == 0) { startSystemServer = true;// 命中 --application,是独立应用模式 } else if (strcmp(arg, "--application") == 0) { application = true;// 命中 --nice-name=,自定义进程名 } else if (strncmp(arg, "--nice-name=", 12) == 0) { niceName.setTo(arg + 12);// 既非选项又非 -- 开头的一串,当作要启动的类名 } else if (strncmp(arg, "--", 2) != 0) { className.setTo(arg); break; } else { --i; break; } }// 组装要传给 Java 入口 main() 的参数表(对应 ZygoteInit.main 收到的 argv) Vector<String8> args; if (!className.isEmpty()) {// 非 zygote 模式:只告诉 RuntimeInit 是 application 还是 tool args.add(application ? String8("application") : String8("tool")); runtime.setClassNameAndArgs(className, argc - i, argv + i); } else {// zygote 模式:先创建 dalvik-cache 目录 maybeCreateDalvikCache();// 需要启动 system_server 就把这个标志收进参数表 if (startSystemServer) { args.add(String8("start-system-server")); }// 关键:从系统属性读出本 zygote 支持的 ABI 列表,拼成 --abi-list=// 这就是 ZygoteInit.main() 里 abiList 变量的真正来源 char prop[PROP_VALUE_MAX]; if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) { LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.", ABI_LIST_PROPERTY); return 11; } String8 abiFlag("--abi-list="); abiFlag.append(prop); args.add(abiFlag);// zygote 模式下,剩余参数原样透传给 ZygoteInit.main() for (; i < argc; ++i) { args.add(String8(argv[i])); } }// 设置进程名,方便 ps 里看到 if (!niceName.isEmpty()) { runtime.setArgv0(niceName.string()); set_process_name(niceName.string()); }// zygote 模式:把 Java 入口类固定为 ZygoteInit if (zygote) { runtime.start("com.android.internal.os.ZygoteInit", args, zygote);// 普通工具/应用模式:入口是 RuntimeInit } else if (className) { runtime.start("com.android.internal.os.RuntimeInit", args, zygote);// 两者都没有则报错退出 } else { fprintf(stderr, "Error: no class name or --zygote supplied.\n"); app_usage(); LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied."); return 10; }}这里有三点要说清楚。
- 第一,
argv[0]是程序自身(即/system/bin/app_process64),真正的业务参数从argv[1]起,所以代码里先argc--; argv++;。 - 第二,
runtime.start的第三个参数zygote(布尔)决定了AndroidRuntime是作为「zygote 模式」启动,还是普通应用模式启动——zygote参数主要在startVm内部有if (zygote) {...}专属分支;而onVmCreated走哪条路是靠AppRuntime自己记录的mClassName是否为空来判断的(zygote 模式下mClassName恒为空,所以效果上等价,但判断依据是mClassName而不是zygote本身)。 - 第三,
computeArgBlockSize估算的是从argv[0]到最后一个参数结尾的字节数,这个参数块随后会被setArgv0覆写进程名,所以不能随便缩短。
还有开头那句 init.rc 命令里的 -Xzygote:它以 - 开头但不是 --zygote,所以在 app_process 的第一个参数解析循环里会被当成「给虚拟机的选项」,用 runtime.addOption 收进 VM 参数列表,而不是被后面判断 --zygote/--start-system-server 的那个循环处理。也就是说,-Xzygote 从头到尾都是一个 JVM 启动参数,startVm() 内部专门有一段 if (zygote) {...} 分支来处理这个模式——它和 --zygote(决定 native 层走 ZygoteInit 还是 RuntimeInit)是两套独立的开关,只是名字很像。
AppRuntime 继承自 AndroidRuntime,覆写了 onVmCreated(预缓存要启动的类)和 onZygoteInit(起 binder 线程池)等回调。但真正的启动动作发生在 AndroidRuntime::start。
四、native → Java:AndroidRuntime::start 启动虚拟机
runtime.start("com.android.internal.os.ZygoteInit", args, true) 是跨进 Java 世界的门。AndroidRuntime::start 做四件事:启动虚拟机、触发 onVmCreated、注册 JNI,最后用反射调用 ZygoteInit.main。
// 代码位置 /frameworks/base/core/jni/AndroidRuntime.cpp 第 1007—1110 行// Android 运行时启动入口,className 是要进入的 Java 入口类void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote){// 环境里取出根目录,默认 /system const char* rootDir = getenv("ANDROID_ROOT"); if (rootDir == NULL) { rootDir = "/system"; ... }// 初始化 JNI 调用层 JniInvocation jni_invocation; jni_invocation.Init(NULL); JNIEnv* env;// 关键第一步:启动 Java 虚拟机,之后才有 JNIEnv if (startVm(&mJavaVM, &env, zygote) != 0) { return; }// 触发 onVmCreated 回调,AppRuntime 在这里预缓存入口类 onVmCreated(env);// 关键第二步:注册 Android 的 native 函数表 if (startReg(env) < 0) { ALOGE("Unable to register all android natives\n"); return; }// 准备一个 String 数组:第 0 项放类名,后面放业务参数 jclass stringClass; jobjectArray strArray; jstring classNameStr; stringClass = env->FindClass("java/lang/String"); strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL); classNameStr = env->NewStringUTF(className); env->SetObjectArrayElement(strArray, 0, classNameStr);// 把剩余参数依次放进数组 for (size_t i = 0; i < options.size(); ++i) { jstring optionsStr = env->NewStringUTF(options.itemAt(i).string()); env->SetObjectArrayElement(strArray, i + 1, optionsStr); }// 把 "com.android.internal.os.ZygoteInit" 的 '.' 换成 '/' 再 FindClass char* slashClassName = toSlashClassName(className); jclass startClass = env->FindClass(slashClassName); if (startClass == NULL) { ALOGE("JavaVM unable to locate class '%s'\n", slashClassName); } else {// 找到静态 main 方法 jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V"); if (startMeth == NULL) { ALOGE("JavaVM unable to find main() in '%s'\n", className); } else {// 关键第三步:调用 main(),控制权从这里交到 Java 线程 env->CallStaticVoidMethod(startClass, startMeth, strArray); } } free(slashClassName);// main 返回后虚拟机退出(zygote 模式下 main 不会返回) if (mJavaVM->DetachCurrentThread() != JNI_OK) ALOGW("Warning: unable to detach main thread\n"); if (mJavaVM->DestroyJavaVM() != 0) ALOGW("Warning: VM did not shut down cleanly\n");}这段代码的关键是 strArray 的构造:第 0 项放的是类名 ClassName,后面才放业务参数。视频里讲师反复强调「类名放在第一个位置、后面放的是参数」,就是这个数组。ZygoteInit.main 收到的 argv[0] 其实是类名「com.android.internal.os.ZygoteInit」,真正的 --abi-list=.../--start-system-server 等从 argv[1] 开始。
startVm 才是真正「把虚拟机跑起来」的地方,它设置堆大小、执行模式、GC 等一堆参数,这里只用签名示意,因为它和本文主线关系不大:
// 代码位置 /frameworks/base/core/jni/AndroidRuntime.cpp 第 569—972 行// 启动 Java 虚拟机,zygote=true 时走 zygote 模式的不同参数int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote){ // 设置 -Xms/-Xmx、执行模式、GC 等虚拟机参数后创建 VM ...}再往下 startReg 注册 Android 的 native 函数。Java 里声明成 native 的方法(例如稍后要讲到的 Zygote.nativeForkAndSpecialize)都靠这张注册表,Java 层的 native 关键字的函数才能落到 C++ 实现上——简单说就是「Java 调 native 前必须先把函数注册好,没注册就跑不了」。
启动虚拟机
视频里反复提到:Java 的方法一定要跑在 Java 虚拟机上,没有虚拟机代码就跑不起来。Zygote 是在自己的进程里先把虚拟机 startVm 起来,之后 fork 出来的应用进程和 system_server,都继承了这个虚拟机,因此不需要再 startVm 一次。这正是「应用启动为什么快」的原因:不是启动动作本身快,而是把「启动虚拟机」这一步从每次启动里省掉了。

五、ZygoteInit.main:进入 Java 后的三步
控制权交到 ZygoteInit.main 后,剩下的就是「注册 socket → 预加载 → 进入死循环」三步,外加一个可选的「先 fork system_server」:
// 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 565—622 行// Zygote 进程的 Java 入口public static void main(String argv[]) { try { RuntimeInit.enableDdms();// 记录解析出的参数 boolean startSystemServer = false; String socketName = "zygote"; String abiList = null;// 解析 argv[0] 之后的参数 for (int i = 1; i < argv.length; i++) { if ("start-system-server".equals(argv[i])) { startSystemServer = true; } else if (argv[i].startsWith(ABI_LIST_ARG)) { abiList = argv[i].substring(ABI_LIST_ARG.length()); } else if (argv[i].startsWith(SOCKET_NAME_ARG)) { socketName = argv[i].substring(SOCKET_NAME_ARG.length()); } else { throw new RuntimeException("Unknown command line argument: " + argv[i]); } } if (abiList == null) { throw new RuntimeException("No ABI list supplied."); }// 第一步:注册 zygote socket,等待应用请求连上来 registerZygoteSocket(socketName);// 第二步:预加载类和资源到当前进程 EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START, SystemClock.uptimeMillis()); preload(); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END, SystemClock.uptimeMillis());// 预加载后做一次 GC,给 fork 前腾出干净的环境 gcAndFinalize();// 若带 --start-system-server,先 fork 出 system_server if (startSystemServer) { startSystemServer(abiList, socketName); }// 第三步:进入 runSelectLoop 死循环,等待并处理命令 Log.i(TAG, "Accepting command socket connections"); runSelectLoop(abiList); closeServerSocket(); } catch (MethodAndArgsCaller caller) {// 子进程通过抛这个异常来『跳到』自己的 main() caller.run(); } catch (RuntimeException ex) { Log.e(TAG, "Zygote died with exception", ex); closeServerSocket(); throw ex; }}argv[0] 是类名(在 native 层被填进 strArray 第 0 项),所以参数循环从 i = 1 开始。解析出一个 --abi-list= 是必须的,否则直接抛异常没有后面的事。接下来 registerZygoteSocket、preload、runSelectLoop 就是 Zygote 生命的三个支柱,下面三节依次展开。
这里可以先画一张 ZygoteInit 的结构与三步的关系,把「参数解析 → socket/预加载/循环」的脉络固定下来:

六、预加载:把 Java 环境先装好(为 fork 的写时复制铺垫)
Zygote 是所有应用进程的孵化母体,所以它选择在启动时把「大多数应用都会用到的东西」一次装进自己的进程。preload() 顺序执行五个动作:
// 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 180—191 行// 预加载入口:按顺序装类、资源、OpenGL、动态库、文本资源static void preload() { Log.d(TAG, "begin preload"); preloadClasses(); preloadResources(); preloadOpenGL(); preloadSharedLibraries(); preloadTextResources(); // 让 WebView 服务在 zygote 进程里初始化,便于内存共享 WebViewFactory.prepareWebViewInZygote(); Log.d(TAG, "end preload");}每个动作装什么,看它的实现:
// 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 217—317 行// 加载常用 Java 类:读取 /system/etc/preloaded-classes,逐个 Class.forNameprivate static void preloadClasses() { final VMRuntime runtime = VMRuntime.getRuntime(); InputStream is; try { is = new FileInputStream(PRELOADED_CLASSES); } catch (FileNotFoundException e) { Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + "."); return; } // 预加载期间临时放弃 root,避免静态初始化器以 root 身份运行 ... BufferedReader br = new BufferedReader(new InputStreamReader(is), 256); int count = 0; String line; while ((line = br.readLine()) != null) { line = line.trim(); if (line.startsWith("#") || line.equals("")) { continue; } try { // 用 true 强制触发类初始化 Class.forName(line, true, null); count++; } catch (ClassNotFoundException e) { Log.w(TAG, "Class not found for preloading: " + line); } } Log.i(TAG, "...preloaded " + count + " classes in " + (SystemClock.uptimeMillis() - startTime) + "ms.");} // 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 326—355 行// 加载常用系统资源:drawable 与 color state listprivate static void preloadResources() { final VMRuntime runtime = VMRuntime.getRuntime(); try { mResources = Resources.getSystem(); mResources.startPreloading(); if (PRELOAD_RESOURCES) { // 读取 preloaded_drawables 数组并逐个加载 TypedArray ar = mResources.obtainTypedArray( com.android.internal.R.array.preloaded_drawables); int N = preloadDrawables(runtime, ar); ar.recycle(); // 再读 preloaded_color_state_lists ar = mResources.obtainTypedArray( com.android.internal.R.array.preloaded_color_state_lists); N = preloadColorStateLists(runtime, ar); ar.recycle(); } mResources.finishPreloading(); } catch (RuntimeException e) { Log.w(TAG, "Failure preloading resources", e); }} // 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 200—204 行// 预加载 OpenGL:把 EGL display 提前实例化private static void preloadOpenGL() { if (!SystemProperties.getBoolean(PROPERTY_DISABLE_OPENGL_PRELOADING, false)) { EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); }} // 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 193—198 行// 预加载常用 native 动态库:android、compiler_rt、jnigraphicsprivate static void preloadSharedLibraries() { Log.i(TAG, "Preloading shared libraries..."); System.loadLibrary("android"); System.loadLibrary("compiler_rt"); System.loadLibrary("jnigraphics");} // 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 206—208 行// 预加载文本分字器private static void preloadTextResources() { Hyphenator.init();}到这里,Zygote 进程里已经安装:一台 Java 虚拟机、一批 Framework 常用类、一套系统资源、几个几乎所有应用都会用到的 native 库(android、compiler_rt、jnigraphics)、OpenGL 的 display 实例、文本分字器,外加一个 zygote socket。
七、runSelectLoop:Zygote 的孵化循环
Zygote 启动后主线程不会退出——它进入 runSelectLoop,一个 while(true) 的死循环。课程反复强调「这个 while true 看不到出口」「Zygote 一直在这等待」。靠的是 I/O 多路复用,而不是每来一个连接就开一个线程去接收。
// 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 662—698 行// Zygote 主线程的 select 循环:接受新连接,并逐条处理命令private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {// fds 保存所有被监听的 fd,peers 保存与之一一对应的连接对象 ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>(); ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();// 第 0 个 fd 是 server socket 本身,没有对应的连接,用 null 占位 fds.add(sServerSocket.getFileDescriptor()); peers.add(null);// 死循环,永远等待 while (true) {// 每次先把 fds 转成 poll 需要的 StructPollfd[] 数组 StructPollfd[] pollFds = new StructPollfd[fds.size()]; for (int i = 0; i < pollFds.length; ++i) { pollFds[i] = new StructPollfd(); pollFds[i].fd = fds.get(i);// 只关心「可读」事件 pollFds[i].events = (short) POLLIN; }// 阻塞等待:任何 fd 可读才会返回(-1 表示无限等待) try { Os.poll(pollFds, -1); } catch (ErrnoException ex) { throw new RuntimeException("poll failed", ex); }// 倒序遍历所有 fd for (int i = pollFds.length - 1; i >= 0; --i) {// 该 fd 没有可读事件就跳过 if ((pollFds[i].revents & POLLIN) == 0) { continue; }// 第 0 个 fd:server socket 接受了新连接,建一个 ZygoteConnection if (i == 0) { ZygoteConnection newPeer = acceptCommandPeer(abiList); peers.add(newPeer); fds.add(newPeer.getFileDesciptor());// 其他 fd:是已建立连接上有数据,进入 runOnce 处理一次请求 } else { boolean done = peers.get(i).runOnce(); if (done) { peers.remove(i); fds.remove(i); } } } }}Zygote 主线程把「server socket 的 fd」放进 fds[0],然后 Os.poll 阻塞。一旦有进程(通常来自 AMS)通过 socket 连进来,fds[0] 变为可读,循环走到 i == 0 的分支,acceptCommandPeer 接受连接、新建一个 ZygoteConnection,把它新的 fd 追加进 fds,于是下一次 poll 它也会被监听。等这个连接上真的送来了请求数据(代表 AMS 要创建进程),对应 fd 变为可读,循环走到 i != 0 分支,peers.get(i).runOnce() 完成一次孵化;runOnce 返回 true 表示连接关闭,就把这个 peer 和 fd 从列表里移除,腾出监听位。

I/O 复用:不是每个请求开线程,而是用一个阻塞的 poll 集中监听若干个 fd,谁有事处理谁。Zygote 主线程**几乎永远阻塞在 Os.poll。对比 6.0 之前(4.0/5.0)是用 native 方法做监听,6.0 改成了 Os.poll` 这种多路复用方式。
安全性问题
视频里专门提到 zygote socket「没什么安全机制可判断」「通过地址就能访问」。要理解这一点,看 registerZygoteSocket 的实现就明白了——它并不是某个进程自己 bind 出来的新 socket,而是把 init 早已通过 socket zygote stream 660 root system 创建、并把 fd 传下来的那条 socket 包装起来:
CODE7
socket 是 init 以 root system 权限、660 权限创建在 /dev/socket/zygote 的 UNIX socket,Zygote 只是把 fd 从环境变量捞出来叠了一层 LocalServerSocket。所以它的「保护」主要靠文件系统权限和 uid 校验——任何能连上这个 socket 的进程都能发「创建进程」的请求,这也解释了为什么要靠 ZygoteConnection 里的安全策略(applyUidSecurityPolicy 等)二次拦截。
八、ZygoteConnection.runOnce:读参数并 fork 一个应用进程
AMS 想要创建一个应用进程时,会通过 Process.start 往 zygote socket 写一个参数序列。ZygoteConnection 每个连接对应一次会话,runOnce 把这次会话处理完:
CODE8runOnce 的核心:读参数 → 解析 → 安全校验 → Zygote.forkAndSpecialize。fork 返回后父子分开:pid == 0 是子进程(新应用进程),它清理掉不该继承的 fd 后调用 handleChildProc 进入自己的 ActivityThread.main;pid > 0 是 Zygote 本体,它把这次请求处理完,回到 runSelectLoop 继续 poll 等待下一次。
参数传递:
CODE9
任何一端在 Process.start 里调用 zygoteSendArgsAndGetResult 时,都会先写一个参数个数,再逐行写每个参数。一个典型的请求参数形如 --runtime-args、--setuid=10123、--setgid=10123、--target-sdk-version=23、com.example.app 和若干应用自身参数。ZygoteConnection 拿到后解析出 uid/gid/gids,再交给 forkAndSpecialize。
forkAndSpecialize 是怎么 fork 的
forkAndSpecialize 在 Zygote.java 里是 Java 层的封装,真正干活的是 native:
CODE10
native 层(com_android_internal_os_Zygote.cpp)在这个 nativeForkAndSpecialize 里完成真正的 fork,随后做 setresgid/setgroups/setresuid(同时设置 real/effective/saved 三组 ID,防止子进程用 saved-uid 再提权回 root)、SELinux 设置、关闭 fdsToClose 等,最终把 pid 返回给 Java。本文不深入 native 实现,Java 层的边界已经足够说明「fork 出一个继承 Zygote 环境的应用进程」这件事。
至于什么参数对应哪个字段,ZygoteConnection.Arguments 里把 --setuid、--setgid、--setgroups、--nice-name、--runtime-args、--target-sdk-version 等一一映射成 uid/gid/gids/niceName 字段,供 forkAndSpecialize 调用。
九、forkSystemServer:孵化 system_server
main() 中,如果带 --start-system-server,Zygote 不会直接进入 runSelectLoop,而是先 fork 出 system_server,然后自己才进入等待循环。这一步走的是另一条专门路径:
CODE11
这里出现了几个和普通应用不同的地方。
- 一是
pid == 0时走handleSystemServerProcess,它closeServerSocket(system_server 不再需要 zygote socket)、把进程名设为system_server、对SYSTEMSERVERCLASSPATH做 dexopt,最后经RuntimeInit.zygoteInit进入com.android.server.SystemServer.main。 - 二是有个
waitForSecondaryZygote(当设备存在第二个 zygote 时)。
Zygote.forkSystemServer 同样是 Java 封装 + native 实现:
CODE12
到这里,Zygote 的启动与孵化主线已经闭环:自己的 Java 世界被 AndroidRuntime::start 搭起来,socket 被 registerZygoteSocket 接好,常用资源被 preload 装好,system_server 被 forkSystemServer 先拉起来,最后 Zygote 主线程死在 runSelectLoop 里,通过 acceptCommandPeer 接连接、runOnce 孵化一个个应用进程。
9.1 system_server 挂了,Zygote 为什么也要跟着死
「Zygote 不能被杀死」只是硬币的一面。反过来还有一个源码可验证的机制:Zygote fork 出 system_server 后,就一直通过 SIGCHLD 信号处理器盯着这个 pid(gSystemServerPid)。一旦 system_server 意外退出,Zygote 不会若无其事地继续 runSelectLoop,而是把自己也杀掉——让 init 按 service zygote 的规则重新拉起整条链路。
CODE13gSystemServerPid 在 nativeForkSystemServer 的父进程分支里被赋值:
CODE14
这条规则回答了一个和「Zygote 不能被杀死」对称的问题:如果不是 Zygote 挂,而是它唯一特殊看护的 system_server 挂了,会怎样?答案是 Zygote 主动 kill(getpid(), SIGKILL) 自杀,交给 init 重新拉起整条链。这也是为什么 system_server 崩溃在真机上表现为「黑屏、重新走一遍开机动画」,而不是安静地少了一个进程。
顺带一提,第九节 hasSecondZygote/waitForSecondaryZygote 里的「第二个 zygote」不是巧合命名:app_process 与 app_process64 是两个独立可执行文件,分别以 zygote、zygote64 为 nice name(见 app_main.cpp 里按 __LP64__ 区分的 ZYGOTE_NICE_NAME),各自维护一份 --abi-list=(对应 ro.product.cpu.abilist32/64 属性)。hasSecondZygote 拿设备总的 ro.product.cpu.abilist 与自己这份对比,不相等就说明还有另一位 zygote 要一起启动、一起等——这也是很多支持 32/64 位混跑的设备会同时常驻 zygote 和 zygote64 两个进程的原因。
十、进程 fork
Zygote 能「免费分发环境」,靠的是 Linux 的 fork 写时复制(copy-on-write,COW)。但这份「免费」不是免费的——它有一个明确的内存代价。要理解这个代价,先把内存模型摆出来。
10.1 虚拟地址: 0-3G 是用户空间
一个进程的地址空间被虚拟地址管理。以 32 位 Linux 内核常见的 3G/1G 默认配置来说,地址空间是 0 到 4G:0-3G 归用户空间,3G-4G 归内核空间(内核可以配置成其它划分,64 位地址空间的划分方式完全不同)。代码、so 库、预加载的 class、资源,全部落在用户空间这段虚拟地址里。但虚拟地址只是「映射表」,真正的字节在物理内存(内存条)里,二者通过页(page)机制建立映射:虚拟地址被切成一页页,CPU 通过页表把虚拟页映射到物理页;当某页还没被加载进物理内存、而代码又恰好要去访问它时,就触发一次缺页异常,操作系统这才把该页真正读进物理内存。
所以「加载一个 so」分两步:第一步只是在虚拟地址空间里登记了一段地址;第二步是代码执行到那里、访问触发缺页时才真正占物理内存。这也解释了为什么一个进程虚拟地址能到 3G,但实际物理内存占用只有几百 MB——大部分映射页根本没被触碰。

10.2 fork:只拷贝 mm_struct,页只读共享
Zygote fork 一个应用进程时,Linux 做的事比想象中「轻」。它不复制 3G 的虚拟内容,而是为子进程新建一份进程控制块(task_struct)与内存管理描述符 mm_struct,新页表的表项指向和父进程相同的物理页,并被标记成只读(read-only)。为了让「子进程改动不影响父进程、也不影响其它应用」,这些页保持只读共享;于是:
- 读:父子都直接访问同一份已加载的物理页,不复制。
- 写:触发「写时复制」,内核把该页复制一份,子进程改的是自己的副本。
这就是「读时共享、写时复制」。之所以必须把用户空间标成只读,是因为用户空间的资源(类、资源、图片)如果在父子进程间可写共享,那么一个应用改掉某张预加载图片,所有 fork 出来的应用都会跟着变,系统资源就乱套了。
CODE15
这一下把两件事讲通了。
- 第一,Zygote 不能被杀死:如果 Zygote 挂了,它占用的那批物理内存被释放,所有已 fork 出来、还引用着这批只读页的应用进程就会访问到失效的内存,整个系统瘫痪。所以 Zygote 由 init 守护、优先级极高。
- 第二,应用进程一出生内存就偏大:因为 Copy-on-Write 的前提是只读共享,应用进程不能「按需清零」,只能老老实实继承这一整套预加载环境——哪怕它只做后台服务、完全用不到 UI 资源。
10.3 进程fork收益
好处和坏处是对立的:
- 好处:应用进程启动时不用再加载 JVM、类、资源和 so,这些动作 Zygote 已经替它做过一次,fork 直接继承。这就是「应用启动快」的真正来源——快在省掉了 preload,而不是启动动作本身。
- 坏处:每个应用进程(无论是否需要)都带上了整套预加载资源的「虚拟大小」,物理上通过只读共享省了大部分,但要写的那部分要复制,且进程的 RSS / 虚拟地址都被这套环境撑大。对不碰 UI 的后台服务进程来说,这份「家底」绝大部分是浪费。
素材里用 QQ 音乐举例:它有 9 个进程,其中之一是 qqplayerservice(播放器),这种服务进程完全不和界面打交道,但依然带着 Zygote 全套预加载资源,于是内存偏大。
十一、怎么优化:多进程、服务进程内存、预加载并行
既然应用进程一出生就带着偏大的内存,而系统又按「进程优先级 + 内存大小」来杀进程,那么优化就有两条路:让该有的进程更快见到开屏页,以及让可以被杀的服务进程尽量少占内存。
11.1 多进程:各司其职,别互相拖累
应用开发层面,多进程的一个典型动机是把「重」的初始化挪出主进程。Application.onCreate 在每个进程都会执行一次(N 个进程执行 N 次),所以如果你在 onCreate 里做了大量初始化——比如动态修复播放器 so、加载解码库——那么主进程一启动就会卡在这里,开屏页迟迟不出来。
一个常见做法是把这些重活放进一个独立进程(比如 qqplayerservice),让它的 Application 去做了。主进程的 Application 只做轻量初始化,于是主进程能最快到达开屏页,用户先看到画面,播放器相关的重初始化交给后台进程完成。多进程还带来隔离:每个进程有独立内存上限,进程之间崩溃互不影响,也不容易触发系统的「枪打出头鸟」,整体体验更流畅。
11.2 服务进程内存:越小越不容易被杀
系统杀进程按优先级从低到高:空进程最容易杀,后台其次,服务进程居中,前台进程最难被杀。同时还有一层「内存大小」——Android 的 Low Memory Killer(LMK)在内存紧张时偏向挑「占内存大」的进程开刀。一个后台播放服务(退到后台、锁屏后)容易成为目标,一旦被杀,音乐停、消息丢。
所以对服务进程的优化目标是把内存压到尽可能小。M受限于「fork 继承」的机制,服务进程天然背着 Zygote 的全套环境,能省的是它自己后申请的、与业务无关的一部分,哪怕是优化掉 1-3MB 也有着良好的收益——因为它发生在「一个可能被反复创建/杀掉的进程」上。
11.3 系统侧:预加载并行化
前面看到 Zygote 的 preload() 是单进程、单线程顺序执行的:preloadClasses 大约 1-2 秒,preloadResources 大约 2-3 秒。如果 CPU 是多核,这一长串串行加载完全可以用多个线程并行。让 preloadClasses 和 preloadResources 各占一个线程,总时间就取决于其中较长的那个(2-3 秒),能省出 1-2 秒——对 20 秒左右的开机来说,就是 10% 左右的优化。若是单核 CPU,开多线程反而抢时间片,就不该并行。
CODE16
要点是:这种并行化属于系统 developer 的优化,普通应用开发者无法直接控制 Zygote 的 preload,但它解释了「Zygote 这一段开机时间花在哪、为什么系统能再快一点」。
十二、总结与验证
- app_process 怎么执行 Java 代码:
main()创建AppRuntime,调AndroidRuntime::start("com.android.internal.os.ZygoteInit"),startVm启动 JVM、startReg注册 JNI,最后CallStaticVoidMethod调ZygoteInit.main,native 世界由此跨进 Java。 - Zygote 为什么需要一直活着:
ZygoteInit.main进入runSelectLoop的while(true),靠Os.poll阻塞监听 zygote socket,来一个连接就acceptCommandPeer建ZygoteConnection,有命令就runOnce一次,永远不退出。 - 孵化为什么快:不是启动动作快,而是 fork 的写时复制——Zygote 预先把 JVM、类、资源、so 装好,fork 应用进程时只拷贝
mm_struct、继承只读共享的物理页,省掉每次启动的重加载。
本文由 AgentWorkFlow 生成