一、ART 和 Dalvik
Dalvik 是 Google 为 Android 平台设计的 Java 虚拟机,支持已转换为 .dex(Dalvik Executable)格式的 Java 应用程序运行,.dex 格式是专为 Dalvik 设计的一种压缩格式,适合内存和处理器速度有限的系统。
ART(Android Runtime)在 Android 4.4 中作为开发者选项引入,从 Android 5.0 起成为默认运行时。ART 在应用安装时进行 AOT(Ahead-Of-Time)预编译字节码到机器语言。应用程序安装会变慢,但执行更有效率,启动更快。
- 在 Dalvik 下,应用运行需要解释执行,常用热点代码通过 JIT 将字节码转换为机器码,运行效率较低。而在 ART 环境中,应用在安装时字节码预编译(AOT)成机器码,安装慢了但运行效率提高了。
- ART 占用空间比 Dalvik 大(字节码变为机器码),属于”空间换时间”。
- 预编译可以改善电池续航,因为应用程序每次运行时不用重复编译,减少了 CPU 使用频率,降低了能耗。
1.1 Dexopt 和 DexAot
ART 机制:在安装时首先对 dex 文件进行 Dexopt 验证和优化,转化为 odex 文件,再进行 AOT 提前预编译操作,编译为 AOT 可执行文件(机器码),同时兼容 Dalvik。
Dalvik VM:安装时不处理,在运行时通过 JIT 进行解释执行,其解释执行的文件为 dexopt 进行验证和优化过后的 odex(Optimized dex)文件。
1.2 ART 的混合编译模式(Android 7+)
从 Android 7.0 开始,ART 引入了同时包含 JIT 和 AOT 的混合编译模式:
首次安装 → 解释执行 + Profile 记录 |
Profile Guided Compilation 的关键优势:只编译真正执行的热点代码,而非整个 APK。这减少了安装时间和存储空间占用,同时保持了良好的运行时性能。
二、JVM 类加载器体系
2.1 Java 类加载器层次
JVM 使用双亲委派模型(Parent Delegation Model):
Bootstrap ClassLoader (启动类加载器) |
2.2 双亲委托机制
当某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才会自己去加载。
系统源码(ClassLoader.loadClass 核心逻辑):
protected Class<?> loadClass(String name, boolean resolve) |
为什么需要双亲委派?
- 安全性:防止核心类库被篡改。例如用户自定义的
java.lang.String不会被加载,因为 Bootstrap ClassLoader 已经加载了 rt.jar 中的 String。 - 唯一性:保证同一个类不会被多个 ClassLoader 重复加载。在 JVM 中,一个类的唯一标识是 (ClassLoader + 全限定名),同一个类被不同 ClassLoader 加载视为不同的类型,这将导致
ClassCastException。
2.3 类加载锁与死锁场景
getClassLoadingLock(name) 返回一个对象用作类加载的同步锁。这个锁的作用是防止同一个类被多个线程并发加载——让第一个线程完成加载,其余线程直接使用已加载的结果。
但这也可能造成死锁:
// 死锁场景:两个 ClassLoader 互相加载对方的类 |
Android 的解决方式:classLoader.loadClass(name, false) 中 resolve=false 延迟了类解析,减少了加载过程中的交叉依赖。
三、Android 的 ClassLoader 体系
3.1 核心类与继承关系
// 继承关系 |
// BootClassLoader: 加载 Android Framework 类 |
3.2 PathClassLoader vs DexClassLoader
两者都继承自 BaseDexClassLoader,在 API 26 之前的主要区别:
| 特性 | PathClassLoader | DexClassLoader |
|---|---|---|
| 加载位置 | 已安装 APK (/data/app/) | 任意路径的 JAR/APK |
| optimizedDirectory | 无(使用系统默认) | 需要指定(API 26+ 弃用) |
| 用途 | 加载主 APK 的类 | 加载插件 APK、热修复补丁 |
从 API 26 (Android 8.0) 开始,两者的区别基本消失——都使用 ART 的 DexFile 直接在内存中加载,不再需要 optimizedDirectory。
// PathClassLoader 通常由系统创建 |
3.3 BaseDexClassLoader 的多 DEX 加载
// BaseDexClassLoader 内部使用 DexPathList 管理多个 DEX 文件 |
DexPathList 内部的查找过程:
DexPathList.findClass(name) |
这个遍历机制是 Android 热修复和插件化的核心:将补丁 DEX 文件插入到 dexElements 数组的前面,即可让补丁中的类优先被加载。
四、热修复的类加载原理
4.1 Tinker 方案(全量替换)
Tinker(腾讯开源)通过对比基准 APK 和新 APK,生成 DEX 差分包(.dex patch),然后在运行时合成新的完整 DEX,整体替换旧的 DEX:
1. 下载补丁: patch.dex |
4.2 Sophix 方案(分派热更新)
Sophix(阿里)结合了多种方案:
- 代码修复:底层替换 ArtMethod 结构体内的函数入口(inline hook 级别)
- 资源修复:构造新的 AssetManager,替换 Resource 中的 mAssets 字段
- So 修复:替换 ClassLoader 中的 nativeLibraryDirectories
4.3 核心实现:反射替换 dexElements
public static void installPatch(Context context, String patchDexPath) { |
4.4 注意事项
- Class验证问题:Android 5.0 之前使用 Dalvik 的
dvmVerifyClass验证字节码,较严格。ART 取消了部分验证。补丁如果涉及类结构的重大变化(如新增字段、修改继承关系),可能触发ClassDefNotFound或VerifyError。 - pre-verify 问题:Dalvik 中,如果类 A 引用了类 B,且 A 在 DEX1 中 B 在 DEX2 中,则需要
OPTIMIZED标记。这在 MultiDex 场景下由 Gradle 插件自动处理。 - Native 方法锚定:native 方法注册(RegisterNatives)发生在类加载后。热修复替换了类定义后,native 方法也需要重新注册。
五、Android N+ 的 ClassLoader 变化
5.1 API 24+ 的变化
Android 7.0 (API 24) 对 ClassLoader 做了重要调整:
- 引入了
InMemoryDexClassLoader(API 26),允许直接从ByteBuffer加载 DEX 数据 DexClassLoader的optimizedDirectory参数在 API 26 被标记为@Deprecated- ART 内部使用
DexFile直接在内存中加载,不再需要解压到文件系统
5.2 Instant App 的 ClassLoader
Android Instant App 使用独立的 ClassLoader 层次:
BootClassLoader |
每个 Feature Module 有自己的 Split ClassLoader,但类在模块间是共享的(通过设置 parent 关系)。
六、自定义 ClassLoader 实现
public class CustomClassLoader extends ClassLoader { |
注意事项:
defineClass是ClassLoader中真正将字节流转换为 JVM 中Class对象的方法。- 同一个 ClassLoader 不能对同一个类名调用两次
defineClass(会抛LinkageError)。 - 如果自定义 ClassLoader 需要打破双亲委派(如 Tomcat 的 WebAppClassLoader),应重写
loadClass而非findClass。
七、Android R+ 的 InMemoryDexClassLoader 深入分析
7.1 为什么需要内存加载?
从 Android 8.0 (API 26) 开始引入 InMemoryDexClassLoader,到 Android 11 (API 30) 后逐渐成为热修复和插件化的推荐方案。其核心优势:
- 零文件系统开销:DEX 数据直接从
ByteBuffer加载,无需写入磁盘缓存目录 - 安全性提升:不留下磁盘上的 DEX 文件痕迹,减少逆向风险
- 性能提升:避免磁盘 I/O,加载速度更快
// API 26+: 从内存 ByteBuffer 直接加载 DEX |
7.2 与 PathClassLoader/DexClassLoader 的区别
在 API 26+ 上,三者的底层实现趋同——都通过 DexPathList 管理 DEX 文件,但初始化方式不同:
| 特性 | InMemoryDexClassLoader | DexClassLoader | PathClassLoader |
|---|---|---|---|
| 数据来源 | ByteBuffer(内存) | 文件路径 | 文件路径(系统路径) |
| optimizedDirectory | 不需要 | 已废弃(API 26+) | 不需要 |
| 热修复适用 | 最佳(无需文件权限) | 可用 | 不推荐 |
| API 要求 | 26+ | 所有版本 | 所有版本 |
7.3 InMemoryDexClassLoader 在热修复中的应用
相比传统 DexClassLoader 方案需要将补丁 DEX 先写入磁盘(需要存储权限),InMemoryDexClassLoader 可以直接从网络下载的字节数组构造 ClassLoader:
// 现代热修复方案:下载补丁 → 内存加载 → 反射合并 |
这消除了热修复方案对外部存储权限的依赖,也避免了补丁文件被篡改的风险。
八、类加载死锁场景与解决方案
8.1 经典死锁场景深入分析
在 2.3 节我们提到了类加载死锁的基本场景。这里深入分析实际 Android 开发中可能遇到的三种死锁模式:
模式一:循环依赖死锁
ClassLoader A 加载类 X → X 的静态初始化中引用类 Y |
模式二:跨 ClassLoader 的静态初始化死锁
// 在 Application 启动时最容易触发 |
模式三:插件化中的 ClassLoader 父子反生死锁
当插件 ClassLoader 的 parent 被错误设置,导致插件加载宿主类、宿主又尝试加载插件类时,两者在不同 ClassLoader 的锁上互相等待。
8.2 解决方案
方案 1:延迟类解析(Android 默认)loadClass(name, false) 中 resolve=false 使得类加载和类解析分离。类加载只定义类结构,不解析其依赖;解析推迟到第一次使用时。这减少了锁持有时间。
方案 2:并行类加载的锁细化(Java 7+)getClassLoadingLock(name) 返回的是每个类名独立的锁对象(而非 ClassLoader 级别的全局锁),不同类的加载可以并行进行。
// ClassLoader 源码中的锁细化 |
方案 3:避免静态初始化块中的跨 ClassLoader 引用
在 static {} 块中避免加载其他 ClassLoader 管理的类。延迟初始化通过懒加载单例模式实现。
方案 4:统一 ClassLoader 管理
在插件化架构中,确保公共依赖(如基础库)由同一个 ClassLoader 加载,避免同一个类被多个 ClassLoader 分别加载导致类型不一致和死锁。
九、MultiDex 与类加载
当 APK 的方法数超过 65536 时,需要使用 MultiDex:
// build.gradle |
MultiDex 的工作原理:
- Gradle 将所有 DEX 文件分为
classes.dex(主 DEX,包含启动必需的类)和classes2.dex、classes3.dex等。 MultiDex.install(context)在 Application.attachBaseContext() 中调用。- 安装过程:解压 APK,将
classes2.dex等复制到 data 目录,通过反射将路径注入dexElements数组。
public class MyApplication extends Application { |
八、面试常问题目
Q1: 什么是双亲委派模型?为什么要这样设计?Android 的 ClassLoader 和 JVM 的有何不同?
双亲委派模型要求除了顶层的 Bootstrap ClassLoader 外,所有 ClassLoader 在加载一个类时优先委派给父 ClassLoader。设计目的:(1) 安全性——防止核心类库被篡改;(2) 唯一性——保证 Java 核心库的类型安全,避免同一个类被不同 ClassLoader 加载后引发 ClassCastException。Android 的差异:使用 BootClassLoader → PathClassLoader/DexClassLoader,没有 Extension ClassLoader 这一层。Android 的 BaseDexClassLoader 通过 DexPathList 管理多个 DEX 文件(MultiDex 场景),不遵循严格的双亲委派,在 findClass 中遍历多个 DEX。
Q2: PathClassLoader 和 DexClassLoader 的区别?在什么场景下分别使用?
在 API 26 之前:PathClassLoader 用于加载已安装 APK 中的 DEX(系统自动创建),DexClassLoader 需要指定 optimizedDirectory 用于加载外部路径的 DEX。PathClassLoader 是应用默认的 ClassLoader,DexClassLoader 用于插件化、热修复等加载外部 DEX 的场景。API 26+ 后两者几乎无区别,optimizedDirectory 被废弃,ART 直接在内存中加载 DEX。
Q3: Android 热修复的类加载原理是什么?为什么补丁 DEX 要放在 dexElements 最前面?
热修复的核心是通过反射修改 PathClassLoader 中 DexPathList 的 dexElements 数组,将补丁 DEX 元素插入到数组的最前面。因为 ClassLoader 在 findClass 时从前到后遍历 dexElements,找到匹配的类就立即返回——所以放在前面的补丁 DEX 中的类会被优先加载,从而”覆盖”了原始 DEX 中的同名类。这个机制与双亲委派无关,是在 findClass 阶段通过搜索顺序实现的。
Q4: 为什么会出现 ClassCastException: Foo cannot be cast to Foo?
这种异常出现在同一个类的不同 ClassLoader 加载的场景。在 JVM/ART 中,类的完整标识是 (ClassLoader, package.ClassName)——即使全限定名完全相同,由不同 ClassLoader 加载的类在 JVM 看来是不同的类型。常见场景:(1) 双亲委派被打破后,Parent ClassLoader 和 Child ClassLoader 分别加载了同一个类;(2) 插件化/热修复中,插件和宿主各自打包了相同的第三方库。解决方案是确保公共依赖库只被一个 ClassLoader 加载。
Q5: 类加载过程中有哪些锁?可能导致什么并发问题?
ClassLoader.loadClass 内部通过 getClassLoadingLock(name) 获取同步锁,确保同一个类不会被并发加载多次。主要的锁是类级别的——同一全限定名只允许一个线程执行 defineClass。死锁场景:两个 ClassLoader 在加载各自类时互相依赖对方加载的类,形成循环等待。Android 通过在 loadClass 中使用 resolve=false 延迟类解析,以及限制 native 层的类链接锁粒度来缓解此问题。
参考源码路径:
- Android ClassLoader:
libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java - PathClassLoader:
libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java - DexClassLoader:
libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java - InMemoryDexClassLoader:
libcore/dalvik/src/main/java/dalvik/system/InMemoryDexClassLoader.java - ART 类链接:
art/runtime/class_linker.cc - MultiDex:
androidx/multidex/src/main/java/androidx/multidex/MultiDex.java

