4、Zygote启动流程(下):ZygoteInit.main()函数详解、preload()预加载资源与类、Zygote的Socket创建与监听
好,我们接着往下聊。上一节我们把Zygote的Native世界走了一遍,说白了就是init进程把Zygote拉起来,然后一路跑到Java层。今天这一节,才是真正的重头戏——ZygoteInit.main()。这个函数,是整个Android Java世界的入口。我当年第一次看这段代码的时候,心里就一个感觉:原来系统启动是这么回事。
ZygoteInit.main():Java世界的起点
Zygote进程在Native层完成了初始化后,通过JNI调用了com.android.internal.os.ZygoteInit类的main()方法。嗯,这里要注意,这个类是在frameworks/base/core/java/com/android/internal/os/路径下,属于系统内部API,普通应用是看不到的。
我们直接看代码,我挑核心逻辑讲:
public static void main(String[] argv) {
try {
// 1. 创建Zygote的Socket服务端
ZygoteServer zygoteServer = new ZygoteServer();
// 2. 加载Zygote的启动配置
ZygoteHooks.startZygoteNoThreadCreation();
// 3. 预加载资源和类
preload(bootTimingsTraceLog);
// 4. 完成GC和jit相关初始化
ZygoteHooks.endZygoteNoThreadCreation();
// 5. 进入Socket监听循环
zygoteServer.runSelectLoop(abiList);
// 6. 关闭ServerSocket(正常情况下不会执行到这里)
zygoteServer.closeServerSocket();
} catch (Zygote.MethodAndArgsCaller caller) {
caller.run();
} catch (Throwable ex) {
Log.e(TAG, "Zygote died with exception", ex);
zygoteServer.closeServerSocket();
throw ex;
}
}
这段代码看起来简单,但每一步都藏着玄机。我一个个拆开讲。
preload():为什么Zygote要提前加载这么多东西?
你想想看,如果每次启动一个应用,都要从头加载系统资源和核心类,那得慢成什么样?Zygote的做法很聪明——在启动时就把所有应用共享的东西一次性加载好,然后fork子进程时直接继承。
我个人习惯把preload()做的事情分成三大块:
| 预加载类型 | 具体内容 | 为什么重要 |
|---|---|---|
| 预加载类 | 通过preloadClasses()加载/system/etc/preloaded-classes中列出的类 |
避免每个应用重复加载,减少类加载开销 |
| 预加载资源 | 通过preloadResources()加载系统主题、颜色、布局等 |
应用启动时直接使用,无需重新解析资源 |
| 预加载OpenGL | 通过preloadOpenGL()加载图形库 |
加速图形渲染相关的初始化 |
我们重点看preloadClasses()的实现。我记得有一次在项目中遇到应用启动慢的问题,排查了半天,最后发现是预加载类列表里多了一个第三方库的类,导致Zygote启动时间增加了200ms。嗯,这个坑我踩过。
private static void preloadClasses() {
// 读取预加载类列表文件
// 每一行是一个完整的类名
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(
new FileInputStream(PRELOADED_CLASSES), "UTF-8"));
int count = 0;
String line;
while ((line = br.readLine()) != null) {
// 跳过注释和空行
line = line.trim();
if (line.startsWith("#") || line.isEmpty()) {
continue;
}
try {
Class.forName(line);
count++;
} catch (ClassNotFoundException e) {
Log.w(TAG, "Class not found: " + line);
}
}
Log.i(TAG, "preloaded " + count + " classes");
} catch (IOException e) {
Log.e(TAG, "Error reading " + PRELOADED_CLASSES, e);
} finally {
closeQuietly(br);
}
}
这里有个细节:preloaded-classes文件是在编译时生成的,里面包含了大约3000多个类。为什么是这些类?说白了,就是那些在多个应用中都会被用到的核心类,比如android.app.Activity、android.view.View、java.lang.String等等。
核心要点:预加载的本质是用Zygote的启动时间换取应用的启动速度。Zygote多花1秒,每个应用就能省下几百毫秒。这笔账,怎么算都划算。
Zygote的Socket:应用进程的诞生通道
接下来是Zygote最核心的部分——Socket监听。Zygote创建了一个Unix域Socket(AF_UNIX),专门用来接收SystemServer发来的创建进程请求。
我们看看ZygoteServer是怎么创建这个Socket的:
ZygoteServer() {
// 创建LocalServerSocket
// 监听在 "/dev/socket/zygote" 路径上
mServerSocket = new LocalServerSocket(ZYGOTE_SOCKET);
// 设置文件描述符为非阻塞模式
// 这样在accept时不会阻塞主线程
FileDescriptor fd = mServerSocket.getFileDescriptor();
IoUtils.setBlocking(fd, false);
}
这里有个关键点:Socket的路径是/dev/socket/zygote。为什么放在这里?因为这是init进程创建的socket,Zygote直接继承过来用。我曾经在调试一个权限问题时,发现应用进程创建失败,最后定位到是SELinux策略阻止了Zygote访问这个socket。嗯,这种问题排查起来确实头疼。
创建好Socket后,Zygote进入runSelectLoop()方法。这个方法的核心是一个无限循环,使用select()系统调用来监听多个文件描述符:
void runSelectLoop(String abiList) {
ArrayList<FileDescriptor> fds = new ArrayList<>();
ArrayList<ZygoteConnection> peers = new ArrayList<>();
// 把ServerSocket的fd加入监听列表
fds.add(mServerSocket.getFileDescriptor());
peers.add(null);
while (true) {
// 使用select()监听多个fd
// 当有新的连接请求时,select会返回
StructPollfd[] pollFds = new StructPollfd[fds.size()];
for (int i = 0; i < fds.size(); i++) {
pollFds[i] = new StructPollfd();
pollFds[i].fd = fds.get(i);
pollFds[i].events = (short) POLLIN;
}
try {
Os.poll(pollFds, -1);
} catch (ErrnoException ex) {
throw new RuntimeException("poll failed", ex);
}
// 遍历所有fd,处理就绪的事件
for (int i = pollFds.length - 1; i >= 0; i--) {
if ((pollFds[i].revents & POLLIN) == 0) {
continue;
}
if (i == 0) {
// 新的连接请求
ZygoteConnection newPeer = acceptCommandPeer(abiList);
peers.add(newPeer);
fds.add(newPeer.getFileDesciptor());
} else {
// 已有连接发来数据
boolean done = peers.get(i).runOnce();
if (done) {
peers.remove(i);
fds.remove(i);
}
}
}
}
}
个人经验:这里用poll()而不是select(),是因为poll没有最大文件描述符数量的限制。在实际项目中,如果同时有大量应用启动请求,poll的性能表现会更好。我曾经在压力测试中验证过这一点。
当有新的连接请求时,Zygote会创建一个ZygoteConnection对象来处理。这个对象负责解析客户端发来的参数,然后调用Zygote.forkAndSpecialize()来创建子进程。
避坑指南:Zygote启动中的常见问题
我在多年的Android系统开发中,遇到过几个和Zygote启动相关的典型问题,分享给大家:
- 预加载类过多导致Zygote启动慢:我曾经在一个定制ROM项目中,发现Zygote启动花了将近10秒。排查后发现是预加载类列表里塞了太多不常用的类。解决方案是精简
preloaded-classes文件,只保留真正核心的类。 - Socket权限问题:如果SELinux策略配置不当,Zygote可能无法正常创建或访问Socket。我记得有一次升级Android版本后,应用进程创建全部失败,最后发现是新的SELinux策略限制了Zygote的socket访问权限。
- 内存占用过高:预加载的资源越多,Zygote占用的内存就越大。如果设备内存紧张,可以考虑减少预加载的资源数量。但要注意,这会影响应用的启动速度。
特别注意:千万不要在Zygote的预加载过程中做耗时操作,比如网络请求、数据库操作等。Zygote是单线程的,一旦阻塞,整个系统都无法启动新应用。我曾经见过有人在预加载时做了DNS解析,结果DNS超时导致Zygote卡死,整个系统都起不来。
好了,这一节的内容就到这里。Zygote的main()函数看似简单,但每一步都经过精心设计。预加载机制、Socket监听循环,这些都是Android系统能够快速启动应用的关键。下一节,我们会深入分析Zygote是如何fork出第一个应用进程——SystemServer的,那才是真正的重头戏。