Android Runtime双亲委托模型在ART中的实现
一、双亲委托模型基础概念
1.1 双亲委托模型的定义与核心思想
双亲委托模型是Java类加载机制的核心设计模式,其核心思想是:当一个类加载器收到类加载请求时,它首先不会自己尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上递归,直到最顶层的类加载器 。只有当父类加载器无法完成加载任务时,子类加载器才会尝试自己去加载 。这种模型确保了类加载的层次性和一致性,避免了类的重复加载和安全隐患 。
1.2 模型的层次结构
在Java和Android的类加载体系中,双亲委托模型形成了一个层次分明的结构 。在Android中,主要的类加载器层次包括:
- BootClassLoader:最顶层的类加载器,负责加载系统核心类库,如
java.lang
包下的类和Android框架的核心类 。 - PathClassLoader:用于加载已安装应用的Dex文件,它的父类加载器是
BootClassLoader
。 - DexClassLoader:支持从任意路径加载Dex文件,同样以
BootClassLoader
为父类加载器 。
这种层次结构使得类加载请求能够从底层向上传递,确保核心类的一致性和安全性 。
1.3 模型带来的优势
双亲委托模型为类加载机制带来了多方面的优势 。首先,它保证了类的唯一性,例如java.lang.Object
类无论在哪个类加载器中被请求加载,最终都是由BootClassLoader
加载,确保系统中只有一个版本的Object
类,避免了类冲突 。其次,它提高了系统的安全性,核心类库由最顶层的类加载器加载,防止恶意代码通过自定义类加载器替换系统核心类 。此外,通过缓存已加载的类,减少了重复加载的开销,提高了类加载的效率 。
二、Android类加载器体系架构
2.1 主要类加载器及其职责
在Android系统中,主要的类加载器包括BootClassLoader
、PathClassLoader
和DexClassLoader
,它们各自承担着不同的职责 。
- BootClassLoader:作为系统的根类加载器,负责加载Android系统的核心类库,这些类库位于
/system/framework
目录下,包含了Java语言核心类和Android框架的基础类 。它是ClassLoader
类的内部类,由C++代码在VM启动时创建 。 - PathClassLoader:继承自
BaseDexClassLoader
,用于加载已安装应用的Dex文件 。应用在安装过程中,APK文件会被解压到/data/app
目录,PathClassLoader
从该目录加载应用的主Dex文件(classes.dex
)和可能存在的次级Dex文件 。 - DexClassLoader:同样继承自
BaseDexClassLoader
,但它支持从任意路径加载Dex文件,包括外部存储等位置 。这使得它适用于插件化、热修复等需要动态加载类的场景 。
2.2 类加载器的继承关系
Android的类加载器之间存在明确的继承关系 。BootClassLoader
是所有类加载器的根,它没有父类加载器 。PathClassLoader
和DexClassLoader
都继承自BaseDexClassLoader
,而BaseDexClassLoader
又继承自ClassLoader
类 。这种继承关系为双亲委托模型的实现提供了基础,子类加载器可以通过父类引用调用父类的加载方法,实现委托加载 。
2.3 与Java类加载器体系的异同
Android的类加载器体系与Java标准的类加载器体系既有相似之处,也存在差异 。相似之处在于都采用了双亲委托模型,类加载器之间形成层次结构,核心类由顶层类加载器加载 。不同之处在于,Java标准体系中的顶层类加载器是Bootstrap ClassLoader
(由C++实现),接下来是Extension ClassLoader
和Application ClassLoader
。而Android中没有Extension ClassLoader
,取而代之的是PathClassLoader
和DexClassLoader
,并且Android使用Dex文件格式而非Java的Class文件格式,这导致类加载的具体实现有所不同 。
三、ART运行时环境概述
3.1 ART与Dalvik的区别
Android Runtime(ART)是Android 5.0(API级别21)及以后版本的默认运行时环境,取代了之前的Dalvik虚拟机 。与Dalvik相比,ART具有以下主要区别:
- 预编译(AOT)机制:Dalvik使用JIT(Just-In-Time)编译,在应用运行时将字节码转换为机器码;而ART采用AOT(Ahead-Of-Time)编译,在应用安装时就将Dex字节码编译为机器码,提高了运行效率 。
- 内存管理优化:ART对垃圾回收机制进行了优化,减少了GC暂停时间,提高了应用的响应性 。
- 文件格式:ART使用
.oat
(Optimized Android)文件,它是Dex文件的优化版本,包含了预编译的机器码和元数据;而Dalvik使用.dex
文件 。
这些区别使得ART在性能和用户体验上有了显著提升 。
3.2 ART的类加载机制特点
在ART中,类加载机制与Dalvik有一些不同之处 。由于ART采用AOT预编译,类加载过程中可能涉及到对预编译机器码的处理 。ART会在应用安装时对Dex文件进行优化,生成.oat
文件,类加载时可以直接从.oat
文件中获取预编译的机器码,提高加载和执行效率 。此外,ART对类加载的安全性和兼容性进行了增强,确保不同版本的类能够正确加载和运行 。
3.3 ART与双亲委托模型的关系
ART的类加载机制仍然遵循双亲委托模型,这是Java和Android类加载的核心原则 。在ART中,类加载器的层次结构和委托关系与Dalvik时代基本保持一致 。无论是BootClassLoader
、PathClassLoader
还是DexClassLoader
,在加载类时都会先委托给父类加载器 。ART的实现确保了双亲委托模型在预编译环境下仍然有效,维护了类加载的一致性和安全性 。
四、双亲委托模型在ClassLoader类中的实现
4.1 ClassLoader类的核心结构
ClassLoader
类是Android类加载器体系的基类,定义了类加载的基本框架和核心方法 。其核心结构包括:
- parent字段:指向父类加载器的引用,用于实现委托加载 。
- loadedClasses缓存:存储已加载类的缓存表,避免重复加载 。
- loadClass方法:实现双亲委托模型的核心方法,负责处理类加载请求 。
- findClass方法:在父类加载器无法加载类时,由子类加载器实现该方法来查找和加载类 。
4.2 loadClass方法源码解析
loadClass
方法是实现双亲委托模型的关键,其源码如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 同步锁,确保线程安全
synchronized (getClassLoadingLock(name)) {
// 首先检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent!= null) {
// 如果父类加载器不为null,委托父类加载器加载
c = parent.loadClass(name, false);
} else {
// 父类加载器为null,尝试从Bootstrap类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载该类
}
if (c == null) {
// 父类加载器未找到类,调用自身的findClass方法
long t1 = System.nanoTime();
c = findClass(name);
// 记录类加载时间统计信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
// 如果需要解析类,则进行解析
resolveClass(c);
}
return c;
}
}
该方法的执行流程如下:
- 首先检查请求加载的类是否已经在缓存中(通过
findLoadedClass
方法)。 - 如果未缓存,则将加载请求委托给父类加载器。如果父类加载器存在,则调用其
loadClass
方法;如果父类加载器为null
,则尝试从Bootstrap类加载器加载。 - 如果父类加载器成功加载了类,则返回该类;否则,调用自身的
findClass
方法尝试加载。 - 如果
findClass
方法成功加载了类,则返回该类;否则,抛出ClassNotFoundException
异常。
4.3 findClass方法的作用与实现
findClass
方法在ClassLoader
类中是一个抽象方法,由子类实现,用于在父类加载器无法加载类时,查找和加载类 。在BaseDexClassLoader
(PathClassLoader
和DexClassLoader
的父类)中,findClass
方法的实现如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 在DexPathList中查找类
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
该方法通过DexPathList
对象的findClass
方法在Dex文件列表中查找类。DexPathList
维护了一个Dex文件的列表,每个Dex文件对应一个DexFile
对象,通过遍历这些DexFile
对象来查找类 。
五、BootClassLoader的实现与作用
5.1 BootClassLoader的特殊地位
BootClassLoader
是Android类加载器体系中的最顶层类加载器,它负责加载系统核心类库 。与其他类加载器不同,BootClassLoader
是ClassLoader
类的内部类,由C++代码在VM启动时创建,无法被开发者直接访问 。它没有父类加载器,是整个类加载器层次结构的根 。
5.2 BootClassLoader的初始化过程
在Android系统启动过程中,当ART虚拟机初始化时,会通过JNI调用创建BootClassLoader
实例 。具体来说,在dalvik.system.VMRuntime
类中,有一个本地方法initialize
,该方法会在VM初始化时被调用,其中会创建BootClassLoader
:
// VMRuntime.java
private native void initialize(ClassLoader bootClassLoader,
boolean enableHiddenApiChecks,
boolean ignoreHiddenApiExemptions);
在C++层,对应的JNI方法会创建BootClassLoader
实例并设置其类加载路径:
// 在JNI层实现
static void VMRuntime_initialize(JNIEnv* env, jobject javaThis, jobject bootClassLoader,
jboolean enableHiddenApiChecks,
jboolean ignoreHiddenApiExemptions) {
// 获取JavaVM实例
JavaVM* vm = nullptr;
env->GetJavaVM(&vm);
// 创建BootClassLoader实例
jclass bootClassLoaderClass = env->FindClass("dalvik/system/BootClassLoader");
jmethodID getInstance = env->GetStaticMethodID(bootClassLoaderClass, "getInstance", "()Ldalvik/system/BootClassLoader;");
jobject bootClassLoaderInstance = env->CallStaticObjectMethod(bootClassLoaderClass, getInstance);
// 设置BootClassLoader的类加载路径
// ... 其他初始化操作
}
BootClassLoader
的类加载路径通常是/system/framework
目录下的核心类库文件,这些文件包含了Java和Android的基础类 。
5.3 BootClassLoader在双亲委托中的角色
在双亲委托模型中,BootClassLoader
扮演着最顶层的委托者角色 。当其他类加载器(如PathClassLoader
或DexClassLoader
)收到类加载请求时,会逐级向上委托,最终到达BootClassLoader
。如果请求加载的类属于系统核心类库,BootClassLoader
会尝试加载该类 。只有当BootClassLoader
无法找到对应的类时,请求才会逐级返回,由下一级类加载器尝试加载 。这种机制确保了系统核心类的一致性和安全性 。
六、PathClassLoader与DexClassLoader的委托实现
6.1 PathClassLoader的委托机制
PathClassLoader
是Android应用最常用的类加载器,用于加载已安装应用的Dex文件 。在实现双亲委托模型时,PathClassLoader
通过其父类BaseDexClassLoader
继承了ClassLoader
的loadClass
方法 。当PathClassLoader
收到类加载请求时,首先会检查该类是否已被加载,如果未加载,则将请求委托给父类加载器(通常是BootClassLoader
) 。只有当父类加载器无法加载该类时,PathClassLoader
才会尝试从自己的Dex文件路径中查找并加载 。
6.2 DexClassLoader的委托机制
DexClassLoader
与PathClassLoader
类似,同样继承自BaseDexClassLoader
,因此也遵循相同的双亲委托模型 。不同的是,DexClassLoader
支持从任意路径加载Dex文件,适用于动态加载场景 。当DexClassLoader
收到类加载请求时,同样会先委托给父类加载器,只有在父类加载器无法加载时,才会从自己指定的路径中查找类 。
6.3 二者委托实现的异同点
PathClassLoader
和DexClassLoader
在委托实现上的相同点是都遵循双亲委托模型,通过父类BaseDexClassLoader
继承的loadClass
方法实现委托加载 。不同点在于它们的类加载路径和适用场景 。PathClassLoader
的路径固定为应用安装目录,适用于加载已安装应用的类;而DexClassLoader
的路径可以由开发者指定,适用于动态加载外部Dex文件 。此外,DexClassLoader
在加载类时可能需要进行额外的优化处理,如将Dex文件复制到优化目录 。
七、ART中类加载的JNI实现
7.1 JNI层与Java层的交互
在ART中,类加载涉及到JNI层与Java层的紧密交互 。当Java层的类加载器需要加载类时,某些操作(如查找预编译的机器码)需要通过JNI调用到C++层实现 。例如,在BaseDexClassLoader
的findClass
方法中,会通过DexPathList
对象查找类,而DexPathList
的实现会涉及到JNI调用 。
7.2 关键JNI方法解析
在ART的JNI层,有几个关键方法与类加载密切相关:
- FindClass:在JNI环境中查找类的方法,Java层的类加载器最终会调用该方法来查找类 。
- DefineClass:将类的字节码定义为
Class
对象的方法,用于将从Dex文件中读取的字节码转换为Java可使用的Class
对象 。 - RegisterNatives:注册本地方法的方法,用于将Java方法与C++实现关联起来 。
这些JNI方法在类加载过程中起到了桥梁作用,使得Java层的类加载器能够与底层的ART运行时环境进行交互 。
7.3 类加载过程中的JNI调用流程
在类加载过程中,JNI调用流程大致如下:
- 当Java层的类加载器(如
PathClassLoader
)需要加载类时,会调用findClass
方法 。 findClass
方法通过DexPathList
对象查找类,DexPathList
会遍历其维护的Dex文件列表 。- 对于每个Dex文件,会通过JNI调用到C++层的
DexFile
类,查找对应的类定义 。 - 如果找到类定义,会获取类的字节码或预编译的机器码 。
- 通过JNI调用
DefineClass
方法,将字节码或机器码转换为Java的Class
对象 。 - 将
Class
对象返回给Java层,完成类加载过程 。
这种JNI调用流程确保了Java层的类加载器能够利用ART的底层功能,高效地加载类 。
八、预编译机制对双亲委托的影响
8.1 ART预编译机制概述
ART的预编译机制是其与Dalvik的主要区别之一 。在应用安装过程中,ART会使用dex2oat工具将Dex字节码编译为机器码,生成.oat
文件 。这个.oat
文件包含了应用的所有类的预编译机器码和元数据,使得应用在运行时可以直接执行机器码,提高了性能 。
8.2 预编译对类加载流程的改变
预编译机制对类加载流程产生了一定影响 。在Dalvik时代,类加载主要是从Dex文件中读取字节码,然后解释执行或通过JIT编译为机器码 。而在ART中,类加载时可以直接从.oat
文件中获取预编译的机器码,无需再次编译 。这使得类加载过程更加高效,但也增加了查找和验证预编译机器码的步骤 。
8.3 双亲委托模型在预编译环境下的适应性
双亲委托模型在预编译环境下仍然有效 。虽然类加载过程中涉及到预编译机器码的处理,但委托加载的逻辑并没有改变 。当类加载器收到类加载请求时,仍然会先委托给父类加载器,只有在父类加载器无法加载时,才会尝试从自己的预编译文件中查找类 。ART的实现确保了双亲委托模型在预编译环境下的适应性,维护了类加载的一致性和安全性 。
九、双亲委托模型的安全机制
9.1 防止类的重复加载
双亲委托模型的一个重要安全机制是防止类的重复加载 。由于类加载请求会首先向上委托,当某个类已经被顶层类加载器(如BootClassLoader
)加载后,其他类加载器不会再次加载该类 。例如,java.lang.Object
类由BootClassLoader
加载后,任何其他类加载器请求加载Object
类时,都会直接使用已加载的版本,确保系统中只有一个版本的Object
类,避免了类冲突和安全隐患 。
9.2 保护核心类库安全
双亲委托模型通过将核心类库的加载权限限制在顶层类加载器,保护了核心类库的安全 。例如,java.lang
包下的类只能由BootClassLoader
加载,即使开发者自定义了一个名为java.lang.String
的类,也无法通过自定义类加载器加载,因为委托加载机制会首先将请求委托给BootClassLoader
,而BootClassLoader
会从系统核心类库中加载真正的String
类 。这种机制防止了恶意代码通过替换核心类来破坏系统安全 。
9.3 安全漏洞与防范措施
尽管双亲委托模型提供了一定的安全保障,但仍然存在一些潜在的安全漏洞 。例如,开发者可以通过自定义类加载器打破双亲委托模型,直接加载自己的类,可能导致类冲突或安全风险 。为防范这些漏洞,Android系统采取了多种措施 。例如,限制对系统核心类库路径的访问权限,确保只有系统类加载器能够加载这些路径下的类;对类加载过程进行严格的权限检查,防止非法类的加载 。此外,Android还提供了安全沙箱机制,隔离不同应用的运行环境,进一步增强系统安全性 。
十、双亲委托模型的性能优化
10.1 类加载缓存机制
为提高类加载性能,ART在双亲委托模型的基础上实现了类加载缓存机制 。在ClassLoader
类中,维护了一个已加载类的缓存表(loadedClasses
),用于存储类名和对应的Class
对象 。当类加载器收到类加载请求时,首先会检查这个缓存表,如果类已经被加载,则直接返回缓存中的Class
对象,避免了重复加载的开销 。这种缓存机制在应用运行过程中能够显著提高类加载的效率,尤其是对于频繁使用的类 。
10.2 预编译与缓存的协同作用
ART的预编译机制与类加载缓存机制协同作用,进一步提高了性能 。预编译将Dex字节码转换为机器码,使得类加载时可以直接执行机器码,减少了字节码解释或JIT编译的时间 。而缓存机制则避免了重复加载相同的类,减少了文件读取和解析的开销 。两者结合,使得类加载过程更加高效,应用启动速度和运行性能都得到了提升 。
10.3 委托链长度对性能的影响
双亲委托模型中的委托链长度会对性能产生一定影响 。如果委托链过长,每次类加载都需要经过多个类加载器的委托和查找,会增加类加载的时间 。因此,在设计类加载器层次结构时,应尽量避免不必要的层级,保持委托链的简洁 。在Android中,类加载器的层级结构相对简单,主要是BootClassLoader
、PathClassLoader
和DexClassLoader
,这种简洁的结构有助于提高类加载的性能 。
十一、双亲委托模型的应用场景
11.1 系统核心类加载
双亲委托模型最主要的应用场景是系统核心类的加载 。Android系统的核心类库(如java.lang
、android.app
等包下的类)由BootClassLoader
加载,确保这些类在系统中只有一个版本,并且不会被恶意代码替换 。当应用需要使用这些核心类时,通过双亲委托机制,最终会由BootClassLoader
加载,保证了系统的稳定性和安全性 。
11.2 应用类加载
普通Android应用的类由PathClassLoader
加载 。当应用启动时,PathClassLoader
会从应用安装目录加载主Dex文件和次级Dex文件中的类 。在加载过程中,遵循双亲委托模型,先委托给BootClassLoader
加载系统核心类,只有在父类加载器无法加载时,才会从应用自身的Dex文件中加载 。这种机制确保了应用类与系统核心类的隔离和协作 。
11.3 插件化与热修复技术
在插件化和热修复技术中,双亲委托模型也发挥着重要作用 。插件化开发中,通常使用DexClassLoader
加载插件的Dex文件 。当插件需要使用系统类或主应用类时,通过双亲委托机制,会先委托给父类加载器(如BootClassLoader
或主应用的PathClassLoader
)加载,确保插件与主应用使用相同的系统类版本,避免类冲突 。在热修复中,通过自定义类加载器加载修复后的类,同样遵循双亲委托模型,确保修复类能够正确替换有问题的类 。