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.Activityandroid.view.Viewjava.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的,那才是真正的重头戏。