5. Zygote进程:Android世界的“生命之源”
说实话,Zygote这个名字起得真好。在生物学里,Zygote是受精卵,是生命的起点。在Android系统里,Zygote进程就是所有Java进程的“父亲”。没有它,就没有SystemServer,更没有我们手机里那些花花绿绿的App。
我当年刚接触Android Framework时,一直有个疑问:为什么每次启动App都那么快?后来看了Zygote的源码才恍然大悟——原来它早就把“家底”都准备好了,App进程只需要“复制”一份就行。这就是Zygote的核心思想:预加载 + 快速fork。
5.1 Zygote的启动流程
Zygote是由init进程启动的。具体来说,init.rc里有一段配置:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
class main
priority -20
user root
group root readproc reserved_disk
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart audioserver
onrestart restart cameraserver
onrestart restart media
onrestart restart netd
onrestart restart wificond
注意那个socket zygote stream 660 root system,这就是Zygote用来接收创建进程请求的通信通道。我刚开始看这段配置时,一直没搞懂为什么Zygote要用socket而不是binder。后来才明白——Zygote启动时,binder驱动还没准备好呢。
Zygote的main函数在app_main.cpp里,它会调用AndroidRuntime::start,然后启动Java层的ZygoteInit.main()。整个流程可以概括为:
- init进程启动zygote服务
- app_process加载ZygoteInit类
- 注册Zygote的socket
- 预加载资源和类
- 启动SystemServer
- 进入循环,等待创建App进程的请求
核心要点:Zygote启动后不会退出,它会一直运行在后台,等待socket连接。每次有新的App启动请求,Zygote就fork一个子进程出来。
5.2 预加载资源与类
Zygote为什么这么快?说白了就是“空间换时间”。它在启动时就把常用的类和资源加载到内存里,后面fork出来的子进程直接继承这些数据,不用重新加载。
预加载的代码在ZygoteInit.preload()方法里:
static void preload() {
// 预加载常用的Java类
preloadClasses();
// 预加载系统资源
preloadResources();
// 预加载OpenGL相关
preloadOpenGL();
// 预加载共享库
preloadSharedLibraries();
// 预加载文本连接器
preloadTextResources();
}
我记得有一次排查App启动慢的问题,发现某个App在启动时频繁加载一个系统类。我一看,那个类明明在preloaded-classes文件里。后来发现是那个App用了自定义ClassLoader,绕过了Zygote的预加载缓存。嗯,这里要注意:预加载只对BootClassLoader加载的类有效。
预加载的类列表在frameworks/base/preloaded-classes文件里,大概有几千个类。你想想看,如果每个App启动时都要重新加载这些类,那得多慢?
我的建议:如果你在开发系统应用,尽量使用系统ClassLoader加载类,这样才能享受到Zygote预加载的优化。自定义ClassLoader虽然灵活,但会牺牲启动速度。
5.3 Zygote的Socket通信
Zygote的socket是本地socket(LocalSocket),不是网络socket。它监听在/dev/socket/zygote路径上。当SystemServer或App进程需要创建新进程时,就会通过这个socket发送请求。
通信协议其实很简单,就是一行一行的字符串。比如:
--setuid=1000
--setgid=1000
--setgroups=...
--runtime-args
--nice-name=system_server
--target-sdk-version=29
Zygote收到这些参数后,会解析出uid、gid、进程名等信息,然后调用fork创建子进程。子进程创建完成后,再根据参数执行对应的Java类。
我曾经在调试一个权限问题时,发现某个App的uid不对。查了半天,原来是socket通信时参数解析出了问题。那个参数里有个换行符没处理好,导致uid被截断了。从那以后,我每次处理socket数据都会格外小心——不要相信任何来自socket的数据。
避坑指南:Zygote socket通信是同步阻塞的。如果某个请求处理时间过长,后面的请求都会被阻塞。我曾经遇到过某个App的启动参数特别复杂,导致Zygote处理了将近1秒,结果其他App的启动都被卡住了。所以,尽量保持请求参数简洁。
5.4 fork创建SystemServer与App进程
Zygote最核心的能力就是fork。fork是Linux系统调用,它会创建一个与父进程几乎一模一样的子进程。子进程会继承父进程的内存空间、文件描述符、信号处理等所有资源。
创建SystemServer的代码在ZygoteInit.startSystemServer()里:
private static boolean startSystemServer() {
String args = "--setuid=1000 --setgid=1000 --setgroups=..."
+ "--runtime-args --nice-name=system_server"
+ "com.android.server.SystemServer";
// fork子进程
int pid = Zygote.forkSystemServer();
if (pid == 0) {
// 子进程:执行SystemServer
handleSystemServerProcess(args);
} else {
// 父进程:继续等待
return true;
}
}
创建App进程的逻辑类似,只不过参数是从socket传过来的。Zygote会调用ZygoteConnection.processOneCommand()来处理每个连接请求。
这里有个关键点:fork之后,子进程和父进程共享内存,但采用写时复制(Copy-On-Write)技术。也就是说,只有当子进程修改某个内存页时,才会真正复制一份。这样既节省了内存,又提高了fork速度。
我刚开始做性能优化时,一直想不通为什么Zygote fork出来的进程内存占用那么高。后来才明白,虽然用了写时复制,但子进程还是会继承父进程的虚拟内存空间。如果Zygote预加载了太多东西,每个App进程的虚拟内存占用都会很大。这就是为什么Android 4.4之后引入了Zygote的轻量级预加载策略。
核心要点:Zygote fork创建进程的过程,本质上是一个“复制-替换”的过程。复制父进程的地址空间,然后替换成子进程自己的代码和数据。这个设计让Android能够快速启动应用,同时保持较低的内存占用。
知识体系总览
下面这张图展示了Zygote进程在整个Android系统中的位置和作用:
从这张图可以看得很清楚:Zygote是承上启下的关键节点。它从init进程启动,完成预加载后,通过socket接收请求,然后fork出SystemServer和所有App进程。没有Zygote,整个Android应用层就无从谈起。
好了,关于Zygote进程的核心内容就这些。记住一句话:Zygote是Android Java世界的“盘古”,它开天辟地,然后创造了万物。
公众号:蓝海资料掘金营,微信deep3321