Skip to content

preloadClasses

深入 preloadClasses 的类表读取、静态初始化、root 降权、异常分级、Dex cache 和 JIT profile 收尾。

基于android-17.0.0_r1
AndroidZygoteART源码阅读

preloadClasses ​

SS006 已给出预加载阶段地图;本篇只拆 preloadClasses()。它的输入是 /system/etc/preloaded-classes,消费者是 boot classpath 的类加载器和 ART dex cache,生效时机是 Zygote 第一次 fork 之前。重点不在“加载了很多类”,而在于源码如何处理静态初始化副作用、root 权限、缺失项、native link 错误、文件读取错误和 profile 计数。

1. 输入与owner ​

源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

java
private static final String PRELOADED_CLASSES =
        "/system/etc/preloaded-classes";
private static final int UNPRIVILEGED_UID = 9999;
private static final int ROOT_UID = 0;
private static final int ROOT_GID = 0;

类表文件由系统镜像提供,不是 Zygote 在运行时生成;VMRuntime.getRuntime() 是 ART 侧 owner;ZygoteInit 只负责读取文件、调用 Class.forName 并在 finally 阶段通知 ART 填充 dex cache。

2. 打开与root降权 ​

源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

java
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;
}

final int reuid = Os.getuid();
final int regid = Os.getgid();
boolean droppedPriviliges = false;
if (reuid == ROOT_UID && regid == ROOT_GID) {
    Os.setregid(ROOT_GID, UNPRIVILEGED_GID);
    Os.setreuid(ROOT_UID, UNPRIVILEGED_UID);
    droppedPriviliges = true;
}

文件不存在时函数记录错误并直接返回;这条路径不会进入逐类循环。只有当前 uid/gid 都是 root 才降权,WrapperInit 等非 root 调用者不会被强制切换。降权的目的,是让类静态初始化在接近普通运行权限的上下文中执行,避免预加载期间意外以 root 身份触发受保护操作。

如果 setreuid/setregid 失败会抛出 RuntimeException,跳过类加载;成功降权后,finally 必须恢复 root,因为 Zygote 后续还要执行 fork 和特权初始化。

3. 逐行加载 ​

源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

java
BufferedReader br = new BufferedReader(
        new InputStreamReader(is), Zygote.SOCKET_BUFFER_SIZE);
int count = 0;
int missingLambdaCount = 0;
String line;
while ((line = br.readLine()) != null) {
    line = line.trim();
    if (line.startsWith("#") || line.equals("")) continue;

    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, line);
    try {
        Class.forName(line, true, null);
        count++;
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
    }
}

trim() 后的空行和 # 注释不参与计数;Class.forName(name, true, null) 的第二个参数 true 强制执行静态初始化,第三个参数 null 使用 boot classpath loader。这里的 count 只统计成功完成加载和初始化的条目,不等于文件行数,也不等于所有类都能被每个应用直接访问。

每个类都包围独立 trace section,启动分析可以定位单个类的初始化耗时。Trace.traceEnd() 放在 finally,确保异常也不会遗留未闭合的 trace section。

4. 异常分级 ​

java
} catch (ClassNotFoundException e) {
    if (line.contains("$$Lambda$")) {
        if (LOGGING_DEBUG) missingLambdaCount++;
    } else {
        Log.w(TAG, "Class not found for preloading: " + line);
    }
} catch (UnsatisfiedLinkError e) {
    Log.w(TAG, "Problem preloading " + line + ": " + e);
} catch (Throwable t) {
    Log.e(TAG, "Error preloading " + line + ".", t);
    if (t instanceof Error) throw (Error) t;
    if (t instanceof RuntimeException) throw (RuntimeException) t;
    throw new RuntimeException(t);
}

异常处理体现了不同失败成本:

  • 普通 ClassNotFoundException:记录 warning,继续读下一行;
  • $$Lambda$ 缺失:在 debug 日志开启时计数,减少预期生成类噪声;
  • UnsatisfiedLinkError:记录 native 依赖问题,但继续预加载;
  • 其他 Error/RuntimeException:重新抛出,静态初始化的严重错误会终止 Zygote 启动;
  • 文件读取 IOException:在外层记录错误,随后仍执行 finally 收尾。

因此“日志中有 preload warning”不能直接推断 Zygote 退出;要看异常类型和是否重新抛出。

5. Dex缓存收尾 ​

源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

java
finally {
    IoUtils.closeQuietly(is);
    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadDexCaches");
    runtime.preloadDexCaches();
    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);

    if (shouldProfileBootClasspath()) {
        Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "ResetJitCounters");
        VMRuntime.resetJitCounters();
        Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
    }
    if (droppedPriviliges) {
        Os.setreuid(ROOT_UID, ROOT_UID);
        Os.setregid(ROOT_GID, ROOT_GID);
    }
}

IoUtils.closeQuietly 关闭输入流;preloadDexCaches() 把已加载类、字段和方法写入 ART 的 dex cache;profile boot classpath 时重置 JIT counters,让后续统计从“预加载阶段之后”开始。无论文件缺失、读取异常还是逐类异常,finally 都承担这些资源和状态收尾。

6. 主线状态图 ​

7. Profile开关 ​

源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

java
private static boolean shouldProfileBootClasspath() {
    return isExperimentEnabled("profilebootclasspath");
}

isExperimentEnabled 读取 dalvik.vm.profilebootclasspath 和 runtime-native-boot namespace 的持久化 DeviceConfig;Zygote 阶段不能依赖尚未初始化的普通 DeviceConfig 服务。该开关只影响 JIT counter 重置和后续 profile 逻辑,不改变类表输入,也不使缺失类变成成功。

8. 导航与后续 ​

bash
# 输入类表路径和实现文件,输出类加载、异常与收尾位置。
rg -n "PRELOADED_CLASSES|preloadClasses|Class\.forName|preloadDexCaches|resetJitCounters" \
  frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

# 输入权限切换符号,输出降权与恢复路径。
rg -n "UNPRIVILEGED_UID|setreuid|setregid|droppedPrivil" \
  frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

下一篇将分析 preloadResources(),只处理 drawable、color state list、TypedArray recycle 和资源缓存失败;不会重复本文的类表、root 降权和 dex cache。