第一部分:理论基础
1. 类加载基础
1.1 类加载的概念
什么是类加载
类加载(Class Loading)是Java虚拟机(在Android中是ART运行时)将类的字节码文件(.class文件)从磁盘或网络中读取到内存,并转换为Java虚拟机可执行的格式的过程。
简单理解:
- Java源代码(.java)经过编译后变成字节码(.class)
- 字节码文件还不能直接运行,需要被加载到内存中
- 类加载就是把.class文件"搬"到内存中,并转换成可以执行的形式
类加载的时机
类加载不是在程序启动时一次性完成的,而是按需加载(Lazy Loading):
- 主动使用时才加载:只有在首次使用时才加载
- 延迟加载:不需要的类不会被加载,节省内存
- 动态加载:在程序运行过程中随时可以加载新类
类加载的过程
类加载包含以下几个步骤:
加载(Loading)
↓
验证(Verification)
↓
准备(Preparation)
↓
解析(Resolution)
↓
初始化(Initialization)
详细过程将在后面章节介绍
Android中的类加载
Android中类加载的特殊之处:
- 从DEX文件加载:Android不直接使用.class文件,而是使用DEX(Dalvik Executable)格式
- APK打包:多个.class文件被打包成一个DEX文件,再打包到APK中
- 优化格式:DEX格式针对移动设备优化,体积更小,加载更快
- 多DEX支持:当方法数超过65536时,会生成多个DEX文件
1.2 类的生命周期
一个类从被加载到虚拟机内存开始,到卸载出内存为止,经历以下生命周期:
加载(Loading)
↓
验证(Verification)
↓
准备(Preparation)
↓
解析(Resolution)
↓
初始化(Initialization)
↓
使用(Using)
↓
卸载(Unloading)
1. 加载(Loading)
将类的字节码文件(DEX文件)加载到内存中。
做什么:
- 读取DEX文件
- 创建类的二进制数据结构
- 在方法区创建代表这个类的Class对象
结果:类的Class对象创建完成(但还不能使用)
2. 验证(Verification)
确保加载的类符合规范,不会危害虚拟机安全。
验证内容:
- 文件格式验证:DEX文件格式是否正确
- 元数据验证:类的继承关系、字段、方法是否正确
- 字节码验证:方法体中的字节码是否合法
- 符号引用验证:引用的类、字段、方法是否存在
3. 准备(Preparation)
为类的静态变量分配内存,并设置默认初始值。
注意:
- 只分配内存,不赋值(赋值在初始化阶段)
- 基本类型设置默认值(int=0, boolean=false等)
- final static变量如果直接赋值,会在准备阶段就赋值
// 示例
public class MyClass {
static int a = 100; // 准备阶段:a=0(默认值)
static final int b = 200; // 准备阶段:b=200(final直接赋值)
}
4. 解析(Resolution)
将常量池中的符号引用替换为直接引用。
什么是符号引用:用字符串描述引用的类、方法、字段 什么是直接引用:直接指向内存中的地址
解析内容:
- 类或接口的解析
- 字段解析
- 方法解析
- 接口方法解析
5. 初始化(Initialization)
执行类的初始化代码,给静态变量赋值,执行静态代码块。
什么时候初始化:
- 创建类的实例(new)
- 访问类的静态变量或静态方法
- 反射调用
- 初始化子类时,父类先初始化
- 虚拟机启动时,指定的主类会初始化
public class MyClass {
static {
System.out.println("静态代码块执行");
}
static int a = 100; // 这里才真正赋值100
public static void main(String[] args) {
// 首次访问会触发初始化
}
}
6. 使用(Using)
类已经被初始化,可以正常使用了。
使用方式:
- 创建对象实例
- 访问静态成员
- 调用方法
7. 卸载(Unloading)
类不再被使用,从内存中移除。
卸载条件(很难满足):
- 类的所有实例都已被回收
- 加载该类的ClassLoader已被回收
- 该类的Class对象没有被任何地方引用
注意:在Android中,类卸载几乎不可能发生,因为:
- 系统类加载器不会被回收
- 大多数Class对象都会被长期引用
Android中类的生命周期特点
- AOT编译:Android 5.0之后,ART使用AOT(Ahead-Of-Time)编译,部分类在安装时就编译成机器码,加载时更快
- DEX验证优化:安装时会对DEX进行验证和优化,运行时验证时间更短
- 类卸载困难:Android中类卸载几乎不可能,需要注意内存管理
1.3 类加载的时机
主动引用(触发初始化)
以下情况会触发类的初始化:
- 创建类的实例
MyClass obj = new MyClass(); // 触发MyClass初始化
- 访问类的静态变量(非常量)
int value = MyClass.staticVar; // 触发MyClass初始化
- 调用类的静态方法
MyClass.staticMethod(); // 触发MyClass初始化
- 反射调用
Class.forName("com.example.MyClass"); // 触发MyClass初始化
- 初始化子类时,父类先初始化
class Parent {}
class Child extends Parent {}
Child child = new Child(); // 先初始化Parent,再初始化Child
- 虚拟机启动时,指定的主类
public class Main {
public static void main(String[] args) {
// Main类会被初始化
}
}
被动引用(不触发初始化)
以下情况不会触发类的初始化:
- 通过子类引用父类的静态字段
class Parent {
static int value = 100;
}
class Child extends Parent {}
int v = Child.value; // 只初始化Parent,不初始化Child
- 通过数组定义来引用类
MyClass[] array = new MyClass[10]; // 不触发MyClass初始化
- 引用常量(编译期常量)
class MyClass {
static final int CONSTANT = 100; // 编译期常量
}
int v = MyClass.CONSTANT; // 不触发MyClass初始化(编译期已确定值)
注意:如果常量不是编译期常量,会触发初始化
class MyClass {
static final int CONSTANT = new Random().nextInt(); // 运行期常量
}
int v = MyClass.CONSTANT; // 会触发MyClass初始化
类加载的触发条件
类加载的时机:
- 类加载发生在初始化之前
- 虚拟机规范没有强制规定什么时候加载,只规定了什么时候初始化
- 通常实现会在需要使用时才加载
Android中的触发:
- 首次访问类时
- 通过反射加载时
- 动态加载DEX时
Android中类加载的时机
Android中类加载的特殊时机:
- 应用启动时:加载主Activity和必要的类
- 按需加载:只有在真正使用时才加载
- 插件加载:通过DexClassLoader加载插件APK时
- 热修复:加载补丁DEX时
2. 类加载的过程
2.1 加载(Loading)
加载的定义
加载是类加载过程的第一个阶段,在这个阶段,虚拟机需要完成以下事情:
- 通过类的全限定名获取定义此类的二进制字节流(DEX文件)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载的过程
步骤详解:
-
获取DEX文件
- 从APK中读取DEX文件
- 或从外部存储加载DEX文件
- 或从网络中下载DEX文件
-
读取字节流
- 读取DEX文件的二进制数据
- 解析DEX文件格式
-
转换为运行时结构
- 将DEX文件的内容转换为内存中的数据结构
- 在方法区创建类的元数据
-
创建Class对象
- 在堆中创建
java.lang.Class对象 - 这个对象作为访问类信息的入口
- 在堆中创建
加载的产物
加载完成后,会得到:
- Class对象:
java.lang.Class的实例,代表这个类 - 类的元数据:存储在方法区,包括:
- 类的全限定名
- 类的父类
- 类实现的接口
- 类的字段
- 类的方法
- 常量池
// 加载后可以通过Class对象访问类信息
Class<?> clazz = MyClass.class;
String className = clazz.getName(); // 获取类名
Class<?> superClass = clazz.getSuperclass(); // 获取父类
数组类的加载
数组类的加载比较特殊:
- 数组类不是由类加载器创建的,而是由虚拟机在运行时动态创建的
- 数组类的元素类型:如果元素类型是引用类型,需要先加载元素类型
- 数组类的类名:例如
[Ljava.lang.String;表示String数组
String[] array = new String[10];
// 数组类的加载:
// 1. 先加载String类
// 2. 然后由虚拟机创建数组类
Android中类的加载(从APK/DEX文件)
Android中类的加载过程:
-
APK安装时
- 提取APK中的classes.dex文件
- 验证DEX文件的合法性
- 优化DEX文件(生成.odex或.oat文件)
-
应用启动时
- 系统创建PathClassLoader
- PathClassLoader负责加载应用的类
-
类首次使用时的加载流程
需要类MyClass ↓ PathClassLoader查找DEX文件 ↓ 找到MyClass的字节码 ↓ 读取到内存 ↓ 创建Class对象 ↓ 完成加载 -
多DEX加载
- Android支持多个DEX文件(classes.dex, classes2.dex, ...)
- PathClassLoader会依次查找所有DEX文件
2.2 验证(Verification)
验证的目的
验证阶段的主要目的是确保被加载的类的正确性,防止恶意代码危害虚拟机。
为什么需要验证:
- 编译后的字节码可能被篡改
- 网络传输的类可能被修改
- 防止恶意代码攻击
验证的过程
验证过程分为四个阶段:
1. 文件格式验证
验证DEX文件的格式是否正确。
验证内容:
- DEX文件魔数是否正确
- 版本号是否支持
- 常量池中的常量类型是否正确
- 文件结构是否完整
如果验证失败:抛出ClassFormatError
2. 元数据验证
验证类的元数据信息是否符合Java语言规范。
验证内容:
- 类是否有父类(除了Object,其他类必须有父类)
- 类的继承关系是否正确(不能继承final类)
- 是否实现了父类或接口的所有抽象方法
- 字段、方法的访问权限是否正确
如果验证失败:抛出IllegalAccessError、AbstractMethodError等
// 示例:元数据验证失败的情况
final class Parent {}
class Child extends Parent { // 错误:不能继承final类
// 编译时就会报错
}
3. 字节码验证
验证方法体中的字节码是否合法。
验证内容:
- 字节码指令是否正确
- 类型转换是否合法
- 跳转指令的目标是否合法
- 方法调用的参数类型和数量是否正确
如果验证失败:抛出VerifyError
// 示例:字节码验证
public void test() {
int a = 10;
long b = a; // 正确:int可以转long
String str = (String) a; // 错误:int不能转String(编译时就会报错)
}
4. 符号引用验证
验证类是否引用了不存在的类、字段、方法。
验证内容:
- 符号引用中描述的类是否存在
- 字段是否存在
- 方法是否存在
- 方法的访问权限是否允许访问
注意:符号引用验证发生在解析阶段,但也可以在这里提前验证。
如果验证失败:抛出NoClassDefFoundError、NoSuchFieldError、NoSuchMethodError等
验证的优化
Android中的验证优化:
- 安装时验证:APK安装时会对DEX进行验证,运行时验证时间更短
- AOT编译:ART在安装时将部分代码编译成机器码,编译时也会验证
- 验证缓存:验证结果会被缓存,避免重复验证
Android中的验证(DEX验证)
Android中验证的特殊之处:
- DEX文件格式验证:验证DEX文件格式,而不是.class文件格式
- DEX优化:安装时会进行DEX优化,生成.odex或.oat文件
- 验证时机:
- 安装时:验证并优化所有DEX
- 运行时:只验证动态加载的DEX(如插件)
2.3 准备(Preparation)
准备的定义
准备阶段是正式为类变量(静态变量)分配内存并设置类变量初始值的阶段。
重要:
- 这里说的"初始值"是数据类型的零值,不是程序中赋的值
- 只有类变量(static变量)会被分配内存,实例变量在对象实例化时分配
准备的过程
具体过程:
-
为类变量分配内存
- 在方法区(Metaspace)为类变量分配内存空间
-
设置初始值
- 基本类型设置零值:int=0, long=0L, boolean=false, float=0.0f, double=0.0d, char='\u0000'
- 引用类型设置null
-
final static变量的特殊处理
- 如果final static变量在编译期就有值,准备阶段就赋值
- 如果final static变量的值在运行期确定,则赋值发生在初始化阶段
public class PreparationExample {
// 普通静态变量
static int a = 100;
// 准备阶段:a = 0(零值)
// 初始化阶段:a = 100(赋值)
// final static 编译期常量
static final int b = 200;
// 准备阶段:b = 200(直接赋值,因为final且编译期确定)
// final static 运行期常量
static final int c = new Random().nextInt();
// 准备阶段:c = 0(零值)
// 初始化阶段:c = 随机值(赋值)
// 引用类型
static String str = "hello";
// 准备阶段:str = null
// 初始化阶段:str = "hello"
}
静态变量的初始化
普通静态变量:
- 准备阶段:分配内存,设置零值
- 初始化阶段:执行赋值语句
final static变量:
- 如果值在编译期确定:准备阶段就赋值
- 如果值在运行期确定:初始化阶段赋值
final变量的处理
final static变量:
// 编译期常量:准备阶段赋值
static final int COMPILE_TIME_CONSTANT = 100;
// 运行期常量:初始化阶段赋值
static final int RUNTIME_CONSTANT = new Random().nextInt();
// 字符串常量:准备阶段赋值(字符串常量在编译期确定)
static final String STRING_CONSTANT = "hello";
Android中的准备阶段
Android中准备阶段的特点:
- 与JVM相同:准备阶段的流程与JVM相同
- 内存区域:在ART的方法区(类似JVM的Metaspace)分配内存
- 优化:ART可能会优化某些静态变量的初始化
2.4 解析(Resolution)
解析的定义
解析阶段是将常量池中的符号引用替换为直接引用的过程。
符号引用:
- 用字符串描述引用的目标
- 例如:
com.example.MyClass、java/lang/Object
直接引用:
- 直接指向目标的指针、偏移量或句柄
- 可以直接定位到内存中的位置
为什么需要解析:
- 编译时不知道类的实际内存地址
- 运行时才知道类的实际位置
- 解析就是将"字符串描述"转换为"内存地址"
解析的时机
解析可以在类加载后立即进行,也可以在符号引用首次使用时进行。
Java虚拟机规范:
- 解析可以在初始化之前或之后进行
- 但某些字节码指令(如invokespecial、invokestatic等)引用的类、字段、方法必须在使用前解析
实际实现:
- 大多数虚拟机选择延迟解析(Lazy Resolution)
- 只有在真正使用时才解析
解析的内容
解析主要解析以下内容:
1. 类或接口的解析
将类或接口的符号引用解析为直接引用。
解析过程:
- 如果符号引用是数组类型,先解析数组元素类型
- 如果不是数组类型,使用当前类的类加载器加载目标类
- 如果目标类是接口,验证当前类是否实现了接口
- 验证访问权限
可能抛出的异常:
ClassNotFoundException:类不存在IllegalAccessError:访问权限不够
// 示例
public class MyClass {
// 符号引用:java.lang.String
// 解析后:指向String类的直接引用
String name;
// 符号引用:com.example.OtherClass
// 解析时:使用当前类加载器加载OtherClass
OtherClass other;
}
2. 字段解析
将字段的符号引用解析为直接引用。
解析过程:
- 先解析字段所属的类
- 在类中查找字段
- 如果字段不存在,在父类中查找
- 验证访问权限
可能抛出的异常:
NoSuchFieldError:字段不存在IllegalAccessError:访问权限不够
// 示例
public class Parent {
public int parentField;
}
public class Child extends Parent {
public void test() {
// 符号引用:Parent.parentField
// 解析过程:
// 1. 先解析Parent类
// 2. 在Parent中查找parentField
// 3. 找到后,转换为直接引用
int value = parentField;
}
}
3. 方法解析
将方法的符号引用解析为直接引用。
解析过程:
- 先解析方法所属的类
- 在类中查找方法(方法名和参数类型)
- 如果方法不存在,在父类中查找
- 验证访问权限
可能抛出的异常:
NoSuchMethodError:方法不存在IllegalAccessError:访问权限不够AbstractMethodError:方法是抽象的
// 示例
public class Parent {
public void parentMethod() {
// ...
}
}
public class Child extends Parent {
public void test() {
// 符号引用:Parent.parentMethod()V
// 解析过程:
// 1. 先解析Parent类
// 2. 在Parent中查找parentMethod方法
// 3. 找到后,转换为直接引用
parentMethod();
}
}
4. 接口方法解析
将接口方法的符号引用解析为直接引用。
解析过程:
- 先解析接口
- 在接口中查找方法
- 如果方法不存在,在父接口中查找
注意:接口方法解析不能查找Object类的方法(接口不能继承类)
解析的异常
解析过程中可能抛出以下异常:
- ClassNotFoundException:类不存在
- NoSuchFieldError:字段不存在
- NoSuchMethodError:方法不存在
- IllegalAccessError:访问权限不够
- AbstractMethodError:方法是抽象的
- IncompatibleClassChangeError:类定义不兼容
Android中的解析
Android中解析的特点:
- DEX文件中的常量池:Android使用DEX文件的常量池,而不是.class文件的常量池
- 解析时机:ART通常采用延迟解析
- 优化:ART在AOT编译时可能会提前解析一些引用
2.5 初始化(Initialization)
初始化的定义
初始化阶段是类加载过程的最后一步,在这个阶段才开始真正执行类中定义的Java程序代码。
初始化做什么:
- 执行类的初始化代码
- 给静态变量赋值
- 执行静态代码块(
static {}) - 如果存在父类,先初始化父类
初始化的时机
类初始化发生在以下情况:
- 创建类的实例
MyClass obj = new MyClass(); // 触发MyClass初始化
- 访问类的静态变量(非常量)
int value = MyClass.staticVar; // 触发MyClass初始化
- 调用类的静态方法
MyClass.staticMethod(); // 触发MyClass初始化
- 反射调用
Class.forName("com.example.MyClass"); // 触发MyClass初始化
- 初始化子类时
class Parent {}
class Child extends Parent {}
new Child(); // 先初始化Parent,再初始化Child
- 虚拟机启动时指定的主类
public class Main {
public static void main(String[] args) {
// Main类会被初始化
}
}
初始化的过程
初始化步骤:
-
如果存在父类,先初始化父类
- 递归初始化父类链
-
按顺序执行类变量的赋值语句和静态代码块
- 按照在源文件中出现的顺序执行
-
执行完成后,类初始化完成
public class Parent {
static {
System.out.println("Parent静态代码块");
}
static int parentVar = 100;
static {
System.out.println("Parent静态代码块2");
}
}
public class Child extends Parent {
static {
System.out.println("Child静态代码块");
}
static int childVar = 200;
}
// 当执行 new Child() 时,初始化顺序:
// 1. Parent静态代码块
// 2. parentVar = 100
// 3. Parent静态代码块2
// 4. Child静态代码块
// 5. childVar = 200
类的静态初始化
什么是类的静态初始化:
- 类的静态初始化包括静态变量的赋值和静态代码块的执行
- 这些代码由编译器自动组织成一个初始化流程
- 不需要程序员手动调用,由虚拟机在类初始化时自动执行
静态初始化的特点:
- 自动执行:由编译器自动收集静态变量赋值和静态代码块,按顺序执行
- 只执行一次:每个类只执行一次,由虚拟机保证线程安全
- 父类优先:子类的静态初始化执行前,父类的静态初始化必须先执行完成
- 不需要实例:不需要类的实例就能执行
public class MyClass {
static int a = 10;
static int b;
static {
b = 20;
System.out.println("静态代码块执行");
}
// 编译器会自动将这些静态初始化代码组织起来:
// 1. 先执行静态变量赋值:a = 10
// 2. 然后执行静态代码块:b = 20, 打印信息
}
静态初始化的内容:
- 类变量的赋值语句
- 静态代码块
- 按照源代码中出现的顺序排列
public class Example {
// 1. 先执行
static int a = 10;
// 2. 然后执行
static {
System.out.println("第一个静态代码块");
}
// 3. 再执行
static int b = 20;
// 4. 最后执行
static {
System.out.println("第二个静态代码块");
}
}
静态初始化的线程安全:
- 虚拟机确保类的静态初始化在多线程环境下只执行一次
- 如果有多个线程同时初始化同一个类,只有一个线程会执行静态初始化
- 其他线程会阻塞等待,直到静态初始化执行完成
// 示例:多线程初始化
public class ThreadSafeInit {
static {
System.out.println("初始化开始");
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("初始化完成");
}
public static void main(String[] args) {
// 多个线程同时访问
for (int i = 0; i < 5; i++) {
new Thread(() -> {
int value = ThreadSafeInit.staticVar; // 触发初始化
}).start();
}
// 输出结果:初始化只会执行一次
}
}
初始化的顺序
初始化顺序规则:
- 父类优先于子类:先初始化父类,再初始化子类
- 按代码顺序:同一类中,按照源代码中出现的顺序初始化
- 静态优先于实例:静态初始化在实例初始化之前
完整示例:
public class GrandParent {
static {
System.out.println("GrandParent静态代码块");
}
static int gpVar = 100;
}
public class Parent extends GrandParent {
static {
System.out.println("Parent静态代码块");
}
static int pVar = 200;
}
public class Child extends Parent {
static {
System.out.println("Child静态代码块");
}
static int cVar = 300;
// 实例初始化
{
System.out.println("Child实例代码块");
}
public Child() {
System.out.println("Child构造函数");
}
public static void main(String[] args) {
new Child();
}
}
// 输出顺序:
// GrandParent静态代码块
// gpVar = 100
// Parent静态代码块
// pVar = 200
// Child静态代码块
// cVar = 300
// Child实例代码块
// Child构造函数
Android中的初始化
Android中初始化的特点:
- 与JVM相同:初始化流程与JVM相同
- 性能优化:ART可能会优化某些初始化过程
- 多线程安全:同样保证静态初始化的线程安全
第二部分:类加载器
3. 类加载器(ClassLoader)
3.1 类加载器的概念
什么是类加载器
类加载器(ClassLoader)是Java虚拟机实现类加载功能的模块,负责在运行时动态加载类。
简单理解:
- 类加载器就是"搬运工",把类从DEX文件"搬"到内存中
- 不同的类加载器负责加载不同来源的类
- 类加载器决定了类的加载方式和加载位置
类加载器的作用
类加载器的主要作用:
- 加载类:从DEX文件或网络中加载类的字节码
- 定义类:将字节码转换为Class对象
- 查找类:在类路径中查找类文件
- 隔离类:不同类加载器加载的类相互隔离
// 类加载器的基本使用
ClassLoader loader = MyClass.class.getClassLoader();
Class<?> clazz = loader.loadClass("com.example.OtherClass");
类加载器的层次结构
类加载器之间存在层次关系(双亲委派模型):
Bootstrap ClassLoader(根类加载器)
↓
Extension ClassLoader(扩展类加载器)
↓
Application ClassLoader(应用类加载器)
↓
Custom ClassLoader(自定义类加载器)
注意:Android中没有Extension ClassLoader
Android中的类加载器层次
Android中的类加载器层次:
BootClassLoader(根类加载器,系统类)
↓
PathClassLoader(应用类加载器,APK中的类)
↓
DexClassLoader(自定义类加载器,动态加载的DEX)
BootClassLoader:
- 加载Android系统框架类
- 用C++实现,Java代码中无法直接访问
- 是其他所有类加载器的父类
PathClassLoader:
- 加载应用APK中的类
- 系统为每个应用创建一个PathClassLoader实例
- 继承自BaseDexClassLoader
DexClassLoader:
- 用于动态加载DEX文件
- 常用于插件化和热修复
- 继承自BaseDexClassLoader
3.2 类加载器的分类
启动类加载器(Bootstrap ClassLoader)
作用:
- 加载Java核心类库(如java.lang.*、java.util.*等)
- 在JVM中由C++实现
特点:
- 是所有类加载器的根
- 在Java代码中无法直接访问(返回null)
- 没有父类加载器
// 获取Bootstrap ClassLoader
ClassLoader loader = String.class.getClassLoader();
System.out.println(loader); // 输出:null(表示Bootstrap ClassLoader)
Android中的对应:
- Android中对应的是BootClassLoader
- 加载Android系统框架类
扩展类加载器(Extension ClassLoader)- Android不使用
作用(仅JVM):
- 加载扩展目录中的类库
- 在JVM中加载
jre/lib/ext目录下的jar包
注意:Android中不使用扩展类加载器,因为:
- Android不使用标准JRE
- Android的类加载机制与JVM不同
- Android没有扩展目录的概念
应用程序类加载器(Application ClassLoader)
作用:
- 加载应用程序类路径(classpath)下的类
- 是程序默认的类加载器
特点:
- 用户可以直接使用
- 是系统类加载器(可以通过
ClassLoader.getSystemClassLoader()获取)
// 获取系统类加载器(Application ClassLoader)
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemLoader); // 输出:PathClassLoader
// 使用系统类加载器加载类
Class<?> clazz = systemLoader.loadClass("com.example.MyClass");
Android中的类加载器(PathClassLoader、DexClassLoader等)
PathClassLoader:
- Android应用默认的类加载器
- 继承自BaseDexClassLoader
- 用于加载APK中的类
// PathClassLoader的使用
// 通常在应用启动时由系统创建
// 可以通过以下方式获取
ClassLoader appLoader = getClassLoader(); // Activity或Context中
ClassLoader appLoader = MyClass.class.getClassLoader();
DexClassLoader:
- 用于动态加载DEX文件
- 继承自BaseDexClassLoader
- 支持从任意路径加载DEX文件
// DexClassLoader的使用
String dexPath = "/sdcard/plugin.dex"; // DEX文件路径
String optimizedDirectory = getCacheDir().getAbsolutePath(); // 优化后的文件目录
String libraryPath = null; // native库路径
ClassLoader parent = getClassLoader(); // 父类加载器
DexClassLoader dexClassLoader = new DexClassLoader(
dexPath,
optimizedDirectory,
libraryPath,
parent
);
// 使用DexClassLoader加载类
Class<?> clazz = dexClassLoader.loadClass("com.plugin.PluginClass");
InMemoryDexClassLoader(Android 8.0+):
- 从内存中加载DEX文件
- 不需要将DEX文件写入磁盘
- 适合安全敏感的场景
// InMemoryDexClassLoader的使用(Android 8.0+)
byte[] dexBytes = ...; // DEX文件的字节数组
ByteBuffer dexBuffer = ByteBuffer.wrap(dexBytes);
InMemoryDexClassLoader loader = new InMemoryDexClassLoader(
dexBuffer,
getClassLoader() // 父类加载器
);
Class<?> clazz = loader.loadClass("com.plugin.PluginClass");
BaseDexClassLoader:
- PathClassLoader和DexClassLoader的基类
- 包含加载DEX文件的核心逻辑
- 一般不直接使用
自定义类加载器(Custom ClassLoader)
什么时候需要自定义类加载器:
- 从非标准位置加载类(如网络、加密文件)
- 实现插件化功能
- 实现热修复功能
- 实现类的隔离
如何自定义:
- 继承ClassLoader(JVM)或BaseDexClassLoader(Android)
- 重写findClass()方法
- 可选:重写loadClass()方法(破坏双亲委派)
示例(将在后面章节详细介绍)
3.3 双亲委派模型
双亲委派模型的定义
双亲委派模型(Parents Delegation Model)是类加载器的一种工作模式。
核心思想:
- 当一个类加载器需要加载类时,先不自己加载,而是委托给父类加载器
- 只有当父类加载器无法加载时,才自己尝试加载
工作流程:
1. 收到加载类的请求
2. 检查这个类是否已经加载过
3. 如果没有加载过,委托给父类加载器
4. 父类加载器也执行相同流程
5. 如果所有父类加载器都无法加载,才由当前加载器加载
双亲委派模型的工作流程
详细流程:
// 伪代码表示双亲委派模型的工作流程
public Class<?> loadClass(String name) {
// 1. 检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 2. 如果有父类加载器,委托给父类加载器
if (parent != null) {
try {
c = parent.loadClass(name);
if (c != null) {
return c;
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载,继续
}
}
// 3. 父类加载器无法加载,自己尝试加载
c = findClass(name);
return c;
}
实际例子:
// 假设要加载 java.lang.String
// 1. 应用类加载器收到请求
// 2. 委托给扩展类加载器
// 3. 扩展类加载器委托给启动类加载器
// 4. 启动类加载器在自己的路径中找到String类,加载并返回
// 5. 应用类加载器得到String类,返回给调用者
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
Class<?> stringClass = appLoader.loadClass("java.lang.String");
// 实际上是由Bootstrap ClassLoader加载的
双亲委派模型的优点
1. 安全性
- 防止核心类被替换
- 例如:防止自定义的java.lang.String替换系统String类
2. 避免重复加载
- 同一个类只会被加载一次
- 由父类加载器加载的类,子类加载器直接使用
3. 统一性
- 保证类的唯一性
- 不同类加载器加载的同一个类,如果是同一个父类加载器加载的,就是同一个类
示例:安全性保护
// 尝试自定义java.lang.String(会失败)
package java.lang;
public class String { // 错误:不允许自定义java.lang包下的类
// ...
}
// 即使创建了这个类,也无法替换系统的String类
// 因为启动类加载器会优先加载系统String类
双亲委派模型的实现
ClassLoader.loadClass()方法的实现:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 加锁,保证线程安全
synchronized (getClassLoadingLock(name)) {
// 2. 检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 3. 如果有父类加载器,委托给父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 4. 没有父类加载器,使用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载
}
// 5. 父类加载器无法加载,自己尝试加载
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
// 记录加载时间
}
}
// 6. 如果需要解析,进行解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
Android中的双亲委派模型
Android中的实现:
Android中的类加载器同样遵循双亲委派模型:
BootClassLoader
↓
PathClassLoader(应用类加载器)
↓
DexClassLoader(自定义类加载器)
BaseDexClassLoader的实现:
// BaseDexClassLoader继承自ClassLoader
// 重写了findClass()方法,用于从DEX文件加载类
// 但仍然使用父类的loadClass()方法,遵循双亲委派模型
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从DEX文件中查找并加载类
// ...
}
验证双亲委派模型:
// 验证Android中的双亲委派模型
ClassLoader appLoader = getClassLoader(); // PathClassLoader
ClassLoader parent = appLoader.getParent(); // BootClassLoader(返回null,因为是C++实现)
// 加载系统类
Class<?> stringClass = appLoader.loadClass("java.lang.String");
// 实际上是由BootClassLoader加载的
// 加载应用类
Class<?> myClass = appLoader.loadClass("com.example.MyClass");
// 由PathClassLoader加载
3.4 类加载器的命名空间
命名空间的概念
命名空间(Namespace)是类加载器识别类的唯一标识。
简单理解:
- 每个类加载器都有自己的"仓库"
- 不同类加载器的"仓库"是隔离的
- 同一个类,如果由不同的类加载器加载,会被视为不同的类
命名空间的作用
1. 类的唯一性
- 类 = 全限定名 + 类加载器
- 同一个类名,不同类加载器加载,就是不同的类
2. 类的隔离
- 不同类加载器加载的类相互隔离
- 防止类冲突
示例:
// 假设有两个类加载器:loader1和loader2
// 它们都加载了 com.example.MyClass
ClassLoader loader1 = new MyClassLoader(...);
ClassLoader loader2 = new MyClassLoader(...);
Class<?> class1 = loader1.loadClass("com.example.MyClass");
Class<?> class2 = loader2.loadClass("com.example.MyClass");
// 即使类名相同,但类加载器不同,所以是不同的类
System.out.println(class1 == class2); // false
System.out.println(class1.equals(class2)); // false
// 不能互相赋值
Object obj1 = class1.newInstance();
Object obj2 = class2.newInstance();
// obj1 = obj2; // 错误:类型不匹配
不同类加载器的命名空间
每个类加载器都有自己的命名空间:
// 系统类加载器的命名空间
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
Class<?> systemClass = systemLoader.loadClass("com.example.MyClass");
// 自定义类加载器的命名空间
ClassLoader customLoader = new DexClassLoader(...);
Class<?> customClass = customLoader.loadClass("com.example.MyClass");
// 不同的命名空间,不同的类
System.out.println(systemClass == customClass); // false
命名空间与类的唯一性
类的唯一性判断:
// 类的唯一性 = 全限定名 + 类加载器
String className = "com.example.MyClass";
ClassLoader loader1 = ...;
ClassLoader loader2 = ...;
Class<?> class1 = loader1.loadClass(className);
Class<?> class2 = loader2.loadClass(className);
// 判断是否相同
boolean isSame = (class1 == class2); // false(类加载器不同)
// 判断类名是否相同
boolean isSameName = class1.getName().equals(class2.getName()); // true(类名相同)
instanceof判断:
// instanceof判断也会考虑类加载器
Object obj1 = class1.newInstance();
Object obj2 = class2.newInstance();
boolean isInstance1 = (obj1 instanceof class1); // true(同一个类加载器)
boolean isInstance2 = (obj1 instanceof class2); // false(不同的类加载器)
Android中的命名空间
Android中命名空间的特点:
- 每个应用的命名空间:每个应用有独立的PathClassLoader,命名空间隔离
- 插件化中的命名空间:插件使用独立的DexClassLoader,与主应用隔离
- 类的隔离:不同命名空间的类不能直接互相访问
示例:插件化中的命名空间隔离:
// 主应用
ClassLoader appLoader = getClassLoader();
Class<?> appClass = appLoader.loadClass("com.app.MainActivity");
// 插件
ClassLoader pluginLoader = new DexClassLoader(pluginPath, ...);
Class<?> pluginClass = pluginLoader.loadClass("com.plugin.PluginActivity");
// 不同的命名空间,完全隔离
System.out.println(appClass == pluginClass); // false(即使类名相同)
4. Android类加载器
4.1 Android类加载器的类型
Android提供了几种类加载器,用于不同的场景。
PathClassLoader(系统类加载器)
作用:
- 加载应用APK中的类
- 是Android应用默认的类加载器
特点:
- 继承自BaseDexClassLoader
- 只能加载已安装APK中的类
- 系统为每个应用创建一个PathClassLoader实例
创建时机:
- 应用启动时由系统自动创建
- 通过
Context.getClassLoader()或Class.getClassLoader()获取
使用示例:
// 获取PathClassLoader
ClassLoader loader = getClassLoader(); // Activity或Context中
// 或者
ClassLoader loader = MyClass.class.getClassLoader();
// 加载应用中的类
Class<?> clazz = loader.loadClass("com.example.MyClass");
源码分析(简化版):
public class PathClassLoader extends BaseDexClassLoader {
// PathClassLoader构造函数
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
// dexPath: APK的路径
// parent: 父类加载器(通常是BootClassLoader)
}
DexClassLoader(动态加载DEX)
作用:
- 用于动态加载DEX文件
- 支持从任意路径加载DEX(如SD卡、网络等)
特点:
- 继承自BaseDexClassLoader
- 可以加载未安装APK中的类
- 常用于插件化和热修复
构造函数:
public DexClassLoader(
String dexPath, // DEX文件路径(可以是多个,用:分隔)
String optimizedDirectory, // 优化后的文件目录(必须是应用私有目录)
String librarySearchPath, // native库搜索路径
ClassLoader parent // 父类加载器
)
使用示例:
// 准备DEX文件路径
String dexPath = "/sdcard/plugin.dex"; // DEX文件路径
// 优化后的文件目录(必须是应用私有目录)
String optimizedDirectory = getCacheDir().getAbsolutePath();
// 或者
String optimizedDirectory = getFilesDir().getAbsolutePath() + "/dex";
// native库路径(如果没有,传null)
String libraryPath = null;
// 父类加载器(通常是应用类加载器)
ClassLoader parent = getClassLoader();
// 创建DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(
dexPath,
optimizedDirectory,
libraryPath,
parent
);
// 使用DexClassLoader加载类
try {
Class<?> pluginClass = dexClassLoader.loadClass("com.plugin.PluginClass");
Object plugin = pluginClass.newInstance();
// 使用插件类
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
注意事项:
-
optimizedDirectory必须是应用私有目录:
// 正确:使用应用私有目录 String optimizedDirectory = getCacheDir().getAbsolutePath(); // 错误:不能使用外部存储的公共目录 String optimizedDirectory = "/sdcard/optimized"; // 会报错 -
权限问题:
// 需要读取外部存储的权限(Android 10之前) <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> // Android 10+需要使用Scoped Storage -
多DEX支持:
// 可以加载多个DEX文件,用:分隔 String dexPath = "/sdcard/plugin1.dex:/sdcard/plugin2.dex"; DexClassLoader loader = new DexClassLoader(dexPath, ...);
InMemoryDexClassLoader(内存中加载DEX)
作用(Android 8.0+):
- 从内存中加载DEX文件
- 不需要将DEX文件写入磁盘
特点:
- 更安全(不留下文件痕迹)
- 适合安全敏感的场景
- 性能可能略低于文件加载
构造函数:
public InMemoryDexClassLoader(
ByteBuffer dexBuffer, // DEX文件的字节缓冲区
ClassLoader parent // 父类加载器
)
使用示例:
// Android 8.0+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 从网络或加密文件读取DEX字节
byte[] dexBytes = loadDexFromNetwork(); // 或其他来源
// 创建ByteBuffer
ByteBuffer dexBuffer = ByteBuffer.wrap(dexBytes);
// 创建InMemoryDexClassLoader
InMemoryDexClassLoader loader = new InMemoryDexClassLoader(
dexBuffer,
getClassLoader()
);
// 加载类
Class<?> clazz = loader.loadClass("com.plugin.PluginClass");
}
适用场景:
- 从网络动态下载DEX
- 从加密文件加载DEX
- 需要临时加载的DEX(不需要持久化)
BaseDexClassLoader(基类)
作用:
- PathClassLoader和DexClassLoader的基类
- 包含加载DEX文件的核心逻辑
特点:
- 一般不直接使用
- 实现了从DEX文件加载类的核心功能
核心方法:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从DEX路径列表中查找类
Class<?> c = pathList.findClass(name, suppressedExceptions);
if (c != null) {
return c;
}
throw new ClassNotFoundException(...);
}
}
DexPathList:
- 维护DEX文件路径列表
- 负责查找和加载DEX文件中的类
4.2 Android类加载器的特点
从DEX文件加载类
DEX文件格式:
- Android使用DEX(Dalvik Executable)格式,而不是.class格式
- DEX是多个.class文件的合并和优化版本
- 体积更小,加载更快
DEX文件的生成:
.java源文件
↓ 编译
.class字节码文件
↓ 打包(dx工具)
classes.dex(DEX文件)
↓ 打包
APK文件
类加载过程:
// 1. 需要加载类 com.example.MyClass
Class<?> clazz = loader.loadClass("com.example.MyClass");
// 2. 类加载器在DEX文件中查找
// - 遍历所有DEX文件
// - 在DEX文件中查找类名
// - 找到后读取类的字节码
// 3. 将字节码转换为Class对象
// 4. 返回Class对象
支持多DEX文件
为什么需要多DEX:
- 单个DEX文件的方法数限制:65536(64K)
- 当方法数超过限制时,需要拆分多个DEX文件
多DEX文件结构:
APK文件
├── classes.dex(主DEX)
├── classes2.dex(第二个DEX)
├── classes3.dex(第三个DEX)
└── ...
多DEX的加载:
- PathClassLoader会自动加载所有DEX文件
- 加载顺序:classes.dex → classes2.dex → classes3.dex → ...
- 查找类时按顺序查找
启用多DEX:
// build.gradle
android {
defaultConfig {
multiDexEnabled true // 启用多DEX
}
}
dependencies {
implementation 'androidx.multidex:multidex:2.0.1'
}
// Application中初始化
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this); // 安装多DEX支持
}
}
类加载的优化(AOT编译)
AOT(Ahead-Of-Time)编译:
- Android 5.0(API 21)引入ART运行时,支持AOT编译
- 在应用安装时,将DEX文件编译成机器码(.oat文件)
- 运行时直接执行机器码,不需要解释执行
AOT编译的好处:
- 启动速度更快
- 运行时性能更好
- 减少内存占用(某些情况下)
AOT编译的过程:
安装APK
↓
提取DEX文件
↓
验证DEX文件
↓
AOT编译(生成.oat文件)
↓
优化(可选)
↓
完成安装
类加载的优化:
- 部分类在安装时就编译成机器码
- 加载时直接使用编译好的机器码
- 未编译的类仍然使用解释执行
Android类加载器与JVM的区别
主要区别:
| 特性 | JVM | Android |
|---|---|---|
| 字节码格式 | .class文件 | .dex文件 |
| 类加载器 | Bootstrap/Extension/App | BootClassLoader/PathClassLoader/DexClassLoader |
| 多文件支持 | 多个.jar文件 | 多个.dex文件 |
| 编译方式 | JIT(即时编译) | AOT(提前编译)+ JIT |
| 扩展类加载器 | 有 | 无 |
详细对比:
-
字节码格式:
- JVM:每个类一个.class文件
- Android:多个类合并成一个DEX文件
-
类加载器层次:
- JVM:Bootstrap → Extension → Application → Custom
- Android:BootClassLoader → PathClassLoader → DexClassLoader
-
类文件组织:
- JVM:类路径(classpath)下的多个jar文件
- Android:APK中的DEX文件
-
优化策略:
- JVM:主要在运行时优化(JIT)
- Android:安装时优化(AOT)+ 运行时优化(JIT)
4.3 Android类加载器的使用
系统类加载器的使用
获取系统类加载器:
// 方式1:通过Context获取
ClassLoader loader = getClassLoader(); // Activity或Context中
// 方式2:通过Class获取
ClassLoader loader = MyClass.class.getClassLoader();
// 方式3:通过系统方法获取
ClassLoader loader = ClassLoader.getSystemClassLoader();
// 方式4:通过Thread获取
ClassLoader loader = Thread.currentThread().getContextClassLoader();
使用系统类加载器加载类:
// 加载应用中的类
ClassLoader loader = getClassLoader();
try {
Class<?> clazz = loader.loadClass("com.example.MyClass");
Object instance = clazz.newInstance();
// 使用实例
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
动态加载DEX文件
完整的动态加载示例:
public class DynamicLoadManager {
private DexClassLoader dexClassLoader;
/**
* 加载DEX文件
*/
public void loadDex(Context context, String dexPath) {
// 1. 检查DEX文件是否存在
File dexFile = new File(dexPath);
if (!dexFile.exists()) {
Log.e("DynamicLoad", "DEX file not found: " + dexPath);
return;
}
// 2. 准备优化目录(必须是应用私有目录)
File optimizedDir = new File(context.getCacheDir(), "dex");
if (!optimizedDir.exists()) {
optimizedDir.mkdirs();
}
String optimizedPath = optimizedDir.getAbsolutePath();
// 3. 创建DexClassLoader
dexClassLoader = new DexClassLoader(
dexPath,
optimizedPath,
null, // libraryPath
context.getClassLoader() // parent
);
Log.d("DynamicLoad", "DEX loaded successfully");
}
/**
* 从DEX中加载类
*/
public Class<?> loadClass(String className) {
if (dexClassLoader == null) {
throw new IllegalStateException("DEX not loaded");
}
try {
return dexClassLoader.loadClass(className);
} catch (ClassNotFoundException e) {
Log.e("DynamicLoad", "Class not found: " + className, e);
return null;
}
}
/**
* 创建类的实例
*/
public Object newInstance(String className) {
Class<?> clazz = loadClass(className);
if (clazz == null) {
return null;
}
try {
return clazz.newInstance();
} catch (InstantiationException e) {
Log.e("DynamicLoad", "Failed to create instance", e);
return null;
} catch (IllegalAccessException e) {
Log.e("DynamicLoad", "Failed to create instance", e);
return null;
}
}
}
使用示例:
// 在Activity中使用
public class MainActivity extends AppCompatActivity {
private DynamicLoadManager loadManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化加载管理器
loadManager = new DynamicLoadManager();
// 加载DEX文件(从SD卡或网络下载)
String dexPath = "/sdcard/plugin.dex";
loadManager.loadDex(this, dexPath);
// 加载并创建类的实例
Object plugin = loadManager.newInstance("com.plugin.PluginClass");
if (plugin != null) {
// 使用插件
}
}
}
插件化开发中的类加载
插件化中的类加载流程:
1. 下载插件APK
↓
2. 提取插件APK中的DEX文件
↓
3. 创建DexClassLoader加载插件DEX
↓
4. 加载插件中的类
↓
5. 创建插件类的实例
↓
6. 调用插件的方法
插件化类加载示例:
public class PluginManager {
private Map<String, DexClassLoader> pluginLoaders = new HashMap<>();
/**
* 加载插件APK
*/
public void loadPlugin(Context context, String apkPath) {
try {
// 1. 提取APK中的DEX文件(简化版,实际需要解压APK)
String dexPath = extractDexFromApk(apkPath);
// 2. 准备优化目录
File optimizedDir = context.getDir("plugin_opt", Context.MODE_PRIVATE);
// 3. 创建DexClassLoader
DexClassLoader loader = new DexClassLoader(
dexPath,
optimizedDir.getAbsolutePath(),
null,
context.getClassLoader()
);
// 4. 保存类加载器
String pluginName = getPluginName(apkPath);
pluginLoaders.put(pluginName, loader);
Log.d("PluginManager", "Plugin loaded: " + pluginName);
} catch (Exception e) {
Log.e("PluginManager", "Failed to load plugin", e);
}
}
/**
* 从插件中加载类
*/
public Class<?> loadPluginClass(String pluginName, String className) {
DexClassLoader loader = pluginLoaders.get(pluginName);
if (loader == null) {
throw new IllegalStateException("Plugin not loaded: " + pluginName);
}
try {
return loader.loadClass(className);
} catch (ClassNotFoundException e) {
Log.e("PluginManager", "Class not found in plugin", e);
return null;
}
}
private String extractDexFromApk(String apkPath) {
// 简化版:假设DEX已经提取
// 实际需要解压APK,提取classes.dex
return apkPath.replace(".apk", ".dex");
}
private String getPluginName(String apkPath) {
File file = new File(apkPath);
return file.getName();
}
}
热修复中的类加载
热修复的类加载流程:
1. 下载补丁DEX文件
↓
2. 创建DexClassLoader加载补丁DEX
↓
3. 加载补丁类(修复后的类)
↓
4. 替换原类(通过反射或其他机制)
↓
5. 后续使用修复后的类
热修复类加载示例(简化版):
public class HotFixManager {
private DexClassLoader patchLoader;
/**
* 加载补丁DEX
*/
public void loadPatch(Context context, String patchPath) {
File optimizedDir = context.getDir("patch_opt", Context.MODE_PRIVATE);
patchLoader = new DexClassLoader(
patchPath,
optimizedDir.getAbsolutePath(),
null,
context.getClassLoader()
);
Log.d("HotFix", "Patch loaded");
}
/**
* 应用补丁(替换类)
*/
public void applyPatch(String className) {
try {
// 1. 从补丁中加载修复后的类
Class<?> patchClass = patchLoader.loadClass(className);
// 2. 获取原类的ClassLoader
Class<?> originalClass = Class.forName(className);
ClassLoader originalLoader = originalClass.getClassLoader();
// 3. 替换类的定义(这里只是示例,实际实现更复杂)
// 需要使用反射修改ClassLoader中的类缓存
replaceClass(originalLoader, className, patchClass);
Log.d("HotFix", "Patch applied: " + className);
} catch (Exception e) {
Log.e("HotFix", "Failed to apply patch", e);
}
}
private void replaceClass(ClassLoader loader, String className, Class<?> newClass) {
// 实际实现需要使用反射修改BaseDexClassLoader的DexPathList
// 这里只是示例,说明思路
// ...
}
}
注意:实际的热修复框架(如Tinker、AndFix)实现更复杂,需要考虑:
- 类的替换机制
- 方法的替换(Native方法)
- 资源的修复
- 版本兼容性
5. 自定义类加载器(Android场景)
5.1 为什么需要自定义类加载器(Android场景)
动态加载APK/DEX(插件化)
插件化的需求:
- 主应用体积过大,需要按需加载功能模块
- 功能模块需要独立开发和更新
- 不重新发布应用就能添加新功能
类加载的作用:
- 使用DexClassLoader加载插件APK中的DEX
- 每个插件使用独立的类加载器,实现隔离
// 插件化场景
public class PluginLoader {
private DexClassLoader pluginLoader;
public void loadPlugin(String pluginApkPath) {
// 创建独立的类加载器加载插件
pluginLoader = new DexClassLoader(
pluginApkPath,
getOptimizedDir(),
null,
getClassLoader() // 父类加载器
);
}
}
热修复(HotFix)
热修复的需求:
- 修复线上Bug,不需要重新发布应用
- 快速响应问题,减少用户影响
类加载的作用:
- 加载补丁DEX文件
- 替换有问题的类
- 使用修复后的类
// 热修复场景
public class HotFixLoader {
public void applyHotFix(String patchDexPath) {
// 加载补丁DEX
DexClassLoader patchLoader = new DexClassLoader(
patchDexPath,
getOptimizedDir(),
null,
getClassLoader()
);
// 替换原类(具体实现更复杂)
replaceClass(patchLoader);
}
}
从非标准位置加载类
需求场景:
- 从网络下载DEX文件并加载
- 从加密文件中加载类
- 从内存中加载类(Android 8.0+)
// 从网络加载
public void loadFromNetwork(String url) {
// 1. 下载DEX文件
byte[] dexBytes = downloadDex(url);
// 2. 保存到本地
File dexFile = saveToLocal(dexBytes);
// 3. 使用DexClassLoader加载
DexClassLoader loader = new DexClassLoader(
dexFile.getAbsolutePath(),
getOptimizedDir(),
null,
getClassLoader()
);
}
// 从内存加载(Android 8.0+)
@RequiresApi(Build.VERSION_CODES.O)
public void loadFromMemory(byte[] dexBytes) {
ByteBuffer buffer = ByteBuffer.wrap(dexBytes);
InMemoryDexClassLoader loader = new InMemoryDexClassLoader(
buffer,
getClassLoader()
);
}
类隔离
类隔离的需求:
- 不同模块使用不同版本的同一个类库
- 防止类冲突
- 插件之间的隔离
实现方式:
- 每个模块使用独立的类加载器
- 不同类加载器加载的类被视为不同的类
// 类隔离示例
public class IsolatedLoader {
private Map<String, DexClassLoader> loaders = new HashMap<>();
public Class<?> loadClass(String module, String className) {
DexClassLoader loader = loaders.get(module);
if (loader == null) {
// 为每个模块创建独立的类加载器
loader = createLoaderForModule(module);
loaders.put(module, loader);
}
return loader.loadClass(className);
}
}
5.2 如何实现自定义类加载器(Android)
继承BaseDexClassLoader或DexClassLoader
方式1:继承BaseDexClassLoader
public class MyClassLoader extends BaseDexClassLoader {
public MyClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
}
// 可以重写方法添加自定义逻辑
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 添加日志、监控等
Log.d("MyClassLoader", "Loading class: " + name);
return super.findClass(name);
}
}
方式2:继承DexClassLoader
public class CustomDexLoader extends DexClassLoader {
public CustomDexLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 自定义逻辑
return super.findClass(name);
}
}
重写findClass()方法
findClass()的作用:
- 查找并加载类
- 如果类不存在,抛出ClassNotFoundException
重写示例:
public class MyClassLoader extends BaseDexClassLoader {
public MyClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1. 添加日志
Log.d("MyClassLoader", "Finding class: " + name);
long startTime = System.currentTimeMillis();
try {
// 2. 调用父类方法查找类
Class<?> clazz = super.findClass(name);
// 3. 记录加载时间
long duration = System.currentTimeMillis() - startTime;
Log.d("MyClassLoader", "Class loaded in " + duration + "ms: " + name);
return clazz;
} catch (ClassNotFoundException e) {
// 4. 记录加载失败
Log.e("MyClassLoader", "Failed to load class: " + name, e);
throw e;
}
}
}
重写loadClass()方法(破坏双亲委派)
loadClass()的作用:
- 实现双亲委派模型
- 先检查是否已加载,然后委托给父类加载器
为什么要重写:
- 实现自己的加载逻辑
- 破坏双亲委派模型(插件化、热修复需要)
重写示例:
public class MyClassLoader extends BaseDexClassLoader {
private Set<String> preferredPackages; // 优先从当前加载器加载的包
public MyClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
preferredPackages = new HashSet<>();
preferredPackages.add("com.plugin"); // 插件包优先从当前加载器加载
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 2. 如果是优先包,先尝试从当前加载器加载
if (isPreferredPackage(name)) {
try {
c = findClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
} catch (ClassNotFoundException e) {
// 当前加载器找不到,继续委托给父类
}
}
// 3. 委托给父类加载器(双亲委派)
try {
if (getParent() != null) {
c = getParent().loadClass(name);
if (c != null) {
return c;
}
}
} catch (ClassNotFoundException e) {
// 父类加载器找不到
}
// 4. 父类加载器找不到,从当前加载器加载
c = findClass(name);
if (resolve) {
resolveClass(c);
}
return c;
}
private boolean isPreferredPackage(String className) {
for (String pkg : preferredPackages) {
if (className.startsWith(pkg)) {
return true;
}
}
return false;
}
}
Android自定义类加载器的实现示例
完整的自定义类加载器示例:
public class CustomClassLoader extends BaseDexClassLoader {
private static final String TAG = "CustomClassLoader";
// 类加载缓存
private Map<String, Class<?>> classCache = new ConcurrentHashMap<>();
// 加载统计
private AtomicInteger loadCount = new AtomicInteger(0);
public CustomClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1. 检查缓存
Class<?> cached = classCache.get(name);
if (cached != null) {
Log.d(TAG, "Class found in cache: " + name);
return cached;
}
// 2. 记录加载开始
long startTime = System.currentTimeMillis();
Log.d(TAG, "Loading class: " + name);
try {
// 3. 调用父类方法加载类
Class<?> clazz = super.findClass(name);
// 4. 缓存类
classCache.put(name, clazz);
// 5. 统计
int count = loadCount.incrementAndGet();
long duration = System.currentTimeMillis() - startTime;
Log.d(TAG, String.format(
"Class loaded successfully [%d]: %s (took %dms)",
count, name, duration
));
return clazz;
} catch (ClassNotFoundException e) {
Log.e(TAG, "Failed to load class: " + name, e);
throw e;
}
}
/**
* 清除类缓存
*/
public void clearCache() {
classCache.clear();
Log.d(TAG, "Class cache cleared");
}
/**
* 获取加载统计
*/
public int getLoadCount() {
return loadCount.get();
}
/**
* 获取缓存的类数量
*/
public int getCachedClassCount() {
return classCache.size();
}
}
使用示例:
// 创建自定义类加载器
File optimizedDir = getDir("dex_opt", Context.MODE_PRIVATE);
CustomClassLoader loader = new CustomClassLoader(
"/sdcard/plugin.dex",
optimizedDir,
null,
getClassLoader()
);
// 加载类
Class<?> clazz = loader.loadClass("com.plugin.PluginClass");
// 查看统计
Log.d(TAG, "Loaded classes: " + loader.getLoadCount());
Log.d(TAG, "Cached classes: " + loader.getCachedClassCount());
5.3 自定义类加载器的应用场景(Android)
插件化开发
插件化中使用自定义类加载器:
public class PluginClassLoader extends BaseDexClassLoader {
private String pluginName;
public PluginClassLoader(String pluginName, String dexPath,
File optimizedDir, ClassLoader parent) {
super(dexPath, optimizedDir, null, parent);
this.pluginName = pluginName;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 插件类优先从插件加载器加载(破坏双亲委派)
if (name.startsWith("com.plugin." + pluginName)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name);
if (resolve) {
resolveClass(c);
}
}
return c;
}
// 其他类委托给父类加载器
return super.loadClass(name, resolve);
}
}
热修复
热修复中使用自定义类加载器:
public class HotFixClassLoader extends BaseDexClassLoader {
private Set<String> patchedClasses; // 已修复的类
public HotFixClassLoader(String patchDexPath, File optimizedDir,
ClassLoader parent) {
super(patchDexPath, optimizedDir, null, parent);
patchedClasses = new HashSet<>();
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 如果类有补丁,优先从补丁加载
if (patchedClasses.contains(name)) {
try {
Class<?> c = findClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
} catch (ClassNotFoundException e) {
// 补丁中没有,使用原类
}
}
// 委托给父类加载器(加载原类)
return super.loadClass(name, resolve);
}
/**
* 标记类已修复
*/
public void markAsPatched(String className) {
patchedClasses.add(className);
}
}
动态加载
动态加载中使用自定义类加载器:
public class DynamicClassLoader extends BaseDexClassLoader {
private Map<String, Long> loadTimes = new HashMap<>(); // 加载时间记录
public DynamicClassLoader(String dexPath, File optimizedDir,
ClassLoader parent) {
super(dexPath, optimizedDir, null, parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
long startTime = System.currentTimeMillis();
Class<?> clazz = super.findClass(name);
long duration = System.currentTimeMillis() - startTime;
loadTimes.put(name, duration);
Log.d("DynamicLoader", "Loaded " + name + " in " + duration + "ms");
return clazz;
}
/**
* 获取类的加载时间
*/
public long getLoadTime(String className) {
return loadTimes.getOrDefault(className, -1L);
}
}
多DEX加载
自定义多DEX加载器:
public class MultiDexClassLoader extends BaseDexClassLoader {
private List<String> dexPaths; // 多个DEX路径
public MultiDexClassLoader(List<String> dexPaths, File optimizedDir,
ClassLoader parent) {
// 将多个DEX路径用:连接
super(joinPaths(dexPaths), optimizedDir, null, parent);
this.dexPaths = dexPaths;
}
private static String joinPaths(List<String> paths) {
return TextUtils.join(":", paths);
}
/**
* 添加额外的DEX路径
*/
public void addDexPath(String dexPath) {
dexPaths.add(dexPath);
// 注意:需要重新创建ClassLoader才能生效
// 这里只是示例
}
}
6. 双亲委派模型的破坏(Android场景)
6.1 为什么需要破坏双亲委派(Android场景)
插件化需要
插件化的问题:
- 插件中的类需要与主应用隔离
- 插件可能需要使用不同版本的同一个类库
- 如果遵循双亲委派,插件类会被父类加载器加载,无法实现隔离
解决方案:
- 破坏双亲委派,插件类优先从插件类加载器加载
- 每个插件使用独立的类加载器
// 插件化需要破坏双亲委派
public class PluginClassLoader extends BaseDexClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 如果是插件包,优先从当前加载器加载
if (name.startsWith("com.plugin.")) {
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name); // 直接加载,不委托给父类
if (resolve) {
resolveClass(c);
}
}
return c;
}
// 其他类仍遵循双亲委派
return super.loadClass(name, resolve);
}
}
热修复需要
热修复的问题:
- 需要替换已有的类
- 如果遵循双亲委派,修复后的类会被父类加载器中的原类覆盖
解决方案:
- 破坏双亲委派,优先加载补丁中的类
- 补丁类加载器作为父类加载器(或使用其他机制)
// 热修复需要破坏双亲委派
public class HotFixClassLoader extends BaseDexClassLoader {
private Set<String> fixedClasses;
public HotFixClassLoader(String patchPath, File optimizedDir,
ClassLoader parent) {
super(patchPath, optimizedDir, null, parent);
fixedClasses = new HashSet<>();
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 如果是已修复的类,优先从补丁加载
if (fixedClasses.contains(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name); // 从补丁加载
if (resolve) {
resolveClass(c);
}
}
return c;
}
// 其他类委托给父类
return super.loadClass(name, resolve);
}
public void addFixedClass(String className) {
fixedClasses.add(className);
}
}
动态加载需要
动态加载的问题:
- 需要从非标准位置加载类
- 需要控制类的加载顺序和优先级
解决方案:
- 自定义类加载逻辑
- 根据需要决定是否破坏双亲委派
6.2 如何破坏双亲委派(Android)
重写loadClass()方法
方式1:优先从当前加载器加载
public class CustomClassLoader extends BaseDexClassLoader {
private Set<String> priorityPackages;
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 2. 如果是优先包,先尝试从当前加载器加载
if (isPriorityPackage(name)) {
try {
c = findClass(name); // 直接加载,不委托
if (resolve) {
resolveClass(c);
}
return c;
} catch (ClassNotFoundException e) {
// 当前加载器找不到,继续委托
}
}
// 3. 委托给父类加载器
if (getParent() != null) {
try {
c = getParent().loadClass(name);
if (c != null) {
return c;
}
} catch (ClassNotFoundException e) {
// 父类找不到
}
}
// 4. 父类找不到,从当前加载器加载
c = findClass(name);
if (resolve) {
resolveClass(c);
}
return c;
}
private boolean isPriorityPackage(String className) {
// 判断是否是优先包
return priorityPackages != null &&
priorityPackages.stream().anyMatch(className::startsWith);
}
}
方式2:完全自定义加载逻辑
public class CustomClassLoader extends BaseDexClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 完全自定义加载逻辑,不遵循双亲委派
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 自定义加载顺序
c = loadFromCustomSource(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
// 最后才委托给父类
if (getParent() != null) {
return getParent().loadClass(name);
}
throw new ClassNotFoundException(name);
}
private Class<?> loadFromCustomSource(String name) {
// 自定义加载逻辑
try {
return findClass(name);
} catch (ClassNotFoundException e) {
return null;
}
}
}
使用反射修改双亲委派
方式:修改父类加载器引用
public class ClassLoaderHelper {
/**
* 修改类加载器的父类加载器
*/
public static void setParent(ClassLoader loader, ClassLoader newParent) {
try {
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(loader, newParent);
} catch (Exception e) {
Log.e("ClassLoaderHelper", "Failed to set parent", e);
}
}
/**
* 获取类加载器的父类加载器
*/
public static ClassLoader getParent(ClassLoader loader) {
try {
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
return (ClassLoader) parentField.get(loader);
} catch (Exception e) {
return null;
}
}
}
使用示例:
// 创建补丁类加载器
DexClassLoader patchLoader = new DexClassLoader(...);
// 修改应用类加载器的父类为补丁类加载器
ClassLoader appLoader = getClassLoader();
ClassLoader originalParent = ClassLoaderHelper.getParent(appLoader);
ClassLoaderHelper.setParent(appLoader, patchLoader);
// 这样应用类加载器会先委托给补丁类加载器
Android中破坏双亲委派的实现
完整的破坏双亲委派示例(插件化场景):
public class PluginClassLoader extends BaseDexClassLoader {
private String pluginPackage; // 插件包名
public PluginClassLoader(String pluginPackage, String dexPath,
File optimizedDir, ClassLoader parent) {
super(dexPath, optimizedDir, null, parent);
this.pluginPackage = pluginPackage;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 2. 如果是插件包中的类,优先从插件加载器加载
if (name.startsWith(pluginPackage)) {
synchronized (getClassLoadingLock(name)) {
// 再次检查(双重检查锁定)
c = findLoadedClass(name);
if (c == null) {
try {
// 从插件加载(破坏双亲委派)
c = findClass(name);
Log.d("PluginLoader", "Loaded from plugin: " + name);
} catch (ClassNotFoundException e) {
// 插件中没有,委托给父类
Log.d("PluginLoader", "Not in plugin, delegating: " + name);
}
}
if (c == null && getParent() != null) {
c = getParent().loadClass(name);
}
if (c == null) {
throw new ClassNotFoundException(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
// 3. 非插件类,遵循双亲委派
return super.loadClass(name, resolve);
}
}
6.3 SPI机制(Android中的使用)
SPI的定义
SPI(Service Provider Interface):
- 服务提供者接口
- 一种服务发现机制
- 可以在不修改代码的情况下,扩展应用程序的功能
核心思想:
- 定义接口
- 实现类在配置文件中声明
- 运行时动态加载实现类
SPI的工作原理
SPI工作流程:
1. 定义服务接口
↓
2. 实现服务接口(可以是多个实现)
↓
3. 在配置文件中声明实现类
↓
4. 使用ServiceLoader加载服务实现
↓
5. 获取并使用服务实现
示例:
// 1. 定义服务接口
public interface PaymentService {
void pay(double amount);
}
// 2. 实现服务接口
public class AlipayService implements PaymentService {
@Override
public void pay(double amount) {
System.out.println("支付宝支付: " + amount);
}
}
public class WechatPayService implements PaymentService {
@Override
public void pay(double amount) {
System.out.println("微信支付: " + amount);
}
}
// 3. 在META-INF/services/com.example.PaymentService文件中声明实现类
// 文件内容:
// com.example.AlipayService
// com.example.WechatPayService
// 4. 使用ServiceLoader加载
ServiceLoader<PaymentService> services = ServiceLoader.load(PaymentService.class);
// 5. 使用服务实现
for (PaymentService service : services) {
service.pay(100.0);
}
SPI与类加载器
SPI为什么需要破坏双亲委派:
- SPI接口在核心库中:由Bootstrap ClassLoader加载
- SPI实现在应用层:由Application ClassLoader加载
- 需要让Bootstrap ClassLoader能够访问Application ClassLoader加载的类
解决方案:
- 使用线程上下文类加载器(Thread Context ClassLoader)
- 在加载SPI实现时,使用应用类加载器而不是Bootstrap ClassLoader
ServiceLoader的实现原理(简化版):
public final class ServiceLoader<S> implements Iterable<S> {
// 使用线程上下文类加载器加载服务实现
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
// 使用指定的类加载器加载
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl; // 使用应用类加载器,而不是Bootstrap ClassLoader
// 从META-INF/services/中读取配置文件
// 使用loader加载实现类
}
}
Android中SPI的使用(ServiceLoader)
Android中SPI的使用:
// 1. 定义服务接口
public interface PluginInterface {
void execute();
}
// 2. 实现服务接口
public class PluginImpl implements PluginInterface {
@Override
public void execute() {
Log.d("Plugin", "Plugin executed");
}
}
// 3. 在assets/META-INF/services/com.example.PluginInterface文件中声明
// 文件内容:com.example.PluginImpl
// 4. 使用ServiceLoader加载(Android中需要使用应用类加载器)
ServiceLoader<PluginInterface> services = ServiceLoader.load(
PluginInterface.class,
getClassLoader() // 指定类加载器
);
// 5. 使用服务实现
for (PluginInterface service : services) {
service.execute();
}
Android中SPI的注意事项:
- 需要指定类加载器:Android中建议显式指定类加载器
- 配置文件位置:可以在assets或jar包的META-INF/services/目录下
- 类加载器的选择:通常使用应用类加载器或自定义类加载器
7. 类的卸载(Android)
7.1 类卸载的条件
类的所有实例都被回收
类要卸载,首先要求该类的所有实例对象都已经被垃圾回收。
// 示例
public class MyClass {
// ...
}
// 创建实例
MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();
// 只有当obj1和obj2都被回收后,MyClass才有可能被卸载
obj1 = null;
obj2 = null;
// GC后,如果MyClass满足其他条件,可能被卸载
加载该类的ClassLoader已被回收
类要卸载,加载该类的ClassLoader必须被回收。
重要:
- 系统类加载器(PathClassLoader)不会被回收
- 因此,由系统类加载器加载的类不会被卸载
- 只有自定义类加载器加载的类才有可能被卸载
// 示例:自定义类加载器加载的类
DexClassLoader customLoader = new DexClassLoader(...);
Class<?> clazz = customLoader.loadClass("com.example.MyClass");
// 只有当customLoader被回收后,clazz对应的类才有可能被卸载
customLoader = null;
// GC后,如果满足其他条件,可能被卸载
该类对应的java.lang.Class对象没有被引用
类要卸载,该类的Class对象不能被任何地方引用。
// 示例
Class<?> clazz = MyClass.class; // Class对象被引用
// 只要clazz还被引用,MyClass就不会被卸载
// 只有当所有引用都被释放
clazz = null;
// 且满足其他条件,才可能被卸载
Android中类卸载的特点
Android中类卸载非常困难:
-
系统类加载器不会被回收:
- PathClassLoader是应用的主要类加载器
- 它不会被回收,因此应用中的大部分类不会被卸载
-
Class对象经常被缓存:
- 很多框架会缓存Class对象
- 例如:反射框架、序列化框架等
-
类的静态变量持有引用:
- 静态变量不会被回收
- 如果类有静态变量,类就不会被卸载
-
实际应用:
- Android中几乎不会发生类卸载
- 应用重启才是真正的"卸载"
7.2 类卸载的过程
类卸载的时机
类卸载发生在以下情况:
-
垃圾回收时:
- 当GC运行时,会检查类是否可以卸载
- 如果满足条件,会卸载类
-
主动卸载:
- 某些情况下,可以主动触发类卸载
- 但Android中很少使用
类卸载的过程
类卸载步骤:
1. GC检测到类满足卸载条件
↓
2. 标记类为可卸载
↓
3. 清理类的元数据
↓
4. 从方法区移除类信息
↓
5. 卸载完成
类卸载的验证
验证类是否被卸载:
// 使用WeakReference监控Class对象
public class ClassUnloadTest {
public static void testClassUnload() {
// 创建自定义类加载器
DexClassLoader loader = new DexClassLoader(...);
// 加载类
Class<?> clazz = loader.loadClass("com.example.TestClass");
// 使用WeakReference监控Class对象
WeakReference<Class<?>> ref = new WeakReference<>(clazz);
// 清除引用
clazz = null;
loader = null;
// 触发GC
System.gc();
System.runFinalization();
System.gc();
// 检查是否被回收
if (ref.get() == null) {
System.out.println("类已被卸载");
} else {
System.out.println("类未被卸载");
}
}
}
Android中类卸载的困难性
为什么Android中类卸载困难:
- PathClassLoader不会被回收:应用的主要类加载器一直存在
- Class对象被大量引用:反射、序列化等会缓存Class对象
- 静态变量持有引用:静态变量不会回收,类就不会卸载
- ART运行时特性:ART的某些优化可能增加类卸载的难度
实际建议:
- 不要依赖类卸载来管理内存
- 合理设计类的生命周期
- 减少不必要的静态变量
- 使用应用重启来清理内存
7.3 类卸载的注意事项(Android)
Android中类卸载的困难性
现实情况:
- Android中类卸载几乎不会发生
- 应用中的类会一直存在,直到应用被杀死
原因:
- 系统类加载器不会被回收
- 大量框架和库会缓存Class对象
- 静态变量持有类的引用
如何促进类卸载
虽然类卸载困难,但可以通过以下方式促进:
-
使用自定义类加载器:
- 自定义类加载器可以被回收
- 它加载的类才有可能被卸载
-
避免静态变量持有引用:
// 不好:静态变量持有对象引用 public class BadExample { static Object cache = new Object(); // 阻止类卸载 } // 好:使用WeakReference public class GoodExample { static WeakReference<Object> cache = new WeakReference<>(new Object()); } -
及时清理Class对象引用:
// 使用完后及时释放 Class<?> clazz = loader.loadClass(...); // 使用clazz clazz = null; // 释放引用 -
避免缓存Class对象:
- 尽量不要缓存Class对象
- 如果必须缓存,使用WeakReference
类卸载的监控
监控类卸载的方法:
// 使用JVM参数监控类卸载(JVM)
// -XX:+TraceClassUnloading
// Android中可以通过反射监控
public class ClassUnloadMonitor {
private static Map<String, WeakReference<Class<?>>> loadedClasses = new HashMap<>();
public static void monitorClass(String className, Class<?> clazz) {
loadedClasses.put(className, new WeakReference<>(clazz));
}
public static void checkUnloadedClasses() {
for (Map.Entry<String, WeakReference<Class<?>>> entry : loadedClasses.entrySet()) {
if (entry.getValue().get() == null) {
Log.d("ClassUnload", "Class unloaded: " + entry.getKey());
}
}
}
}
第三部分:Android实践应用
8. Android类加载的常见问题
8.1 ClassNotFoundException(Android)
产生原因(DEX文件缺失、路径错误等)
常见原因:
-
类名错误:
// 错误的类名 Class<?> clazz = loader.loadClass("com.example.myclass"); // 应该是MyClass -
DEX文件缺失:
- DEX文件不存在
- DEX文件路径错误
-
类不在DEX中:
- 类没有被编译到DEX中
- ProGuard混淆导致类名变化
-
类加载器错误:
- 使用了错误的类加载器
- 类加载器无法访问DEX文件
解决方案
解决方案:
-
检查类名:
// 确保类名正确(包括包名) String className = "com.example.MyClass"; // 完整类名 Class<?> clazz = loader.loadClass(className); -
检查DEX文件:
String dexPath = "/sdcard/plugin.dex"; File dexFile = new File(dexPath); if (!dexFile.exists()) { Log.e("Error", "DEX file not found: " + dexPath); return; } -
检查类是否在DEX中:
// 使用反射检查 try { Class<?> clazz = loader.loadClass(className); } catch (ClassNotFoundException e) { Log.e("Error", "Class not found in DEX: " + className); } -
检查类加载器:
// 确保使用正确的类加载器 ClassLoader loader = getClassLoader(); // 或自定义类加载器
排查方法
排查步骤:
-
查看错误日志:
try { Class<?> clazz = loader.loadClass(className); } catch (ClassNotFoundException e) { Log.e("Error", "ClassNotFoundException", e); e.printStackTrace(); // 查看完整堆栈 } -
检查类加载器:
ClassLoader loader = getClassLoader(); Log.d("Debug", "ClassLoader: " + loader.getClass().getName()); -
列出DEX中的类:
// 使用工具检查DEX文件内容 // 或使用反射尝试加载
Android特有的原因
Android特有的原因:
-
多DEX问题:
- 类可能在classes2.dex中,但类加载器只查找classes.dex
- 需要启用MultiDex
-
ProGuard混淆:
- 类名被混淆,找不到原类名
- 需要在ProGuard规则中保留类
-
Instant Run:
- Instant Run可能导致类加载问题
- 禁用Instant Run后重新编译
8.2 NoClassDefFoundError(Android)
产生原因
NoClassDefFoundError的原因:
- 类在编译时存在,但运行时找不到
- 通常发生在类的依赖类找不到时
常见场景:
-
依赖类缺失:
public class MyClass { private DependencyClass dep; // DependencyClass运行时不存在 } -
初始化失败:
public class MyClass { static { // 初始化失败 throw new RuntimeException("Init failed"); } }
与ClassNotFoundException的区别
区别:
| ClassNotFoundException | NoClassDefFoundError | |
|---|---|---|
| 发生时机 | 加载类时 | 链接类时 |
| 原因 | 类文件不存在 | 类文件存在但链接失败 |
| 检查时机 | 主动加载时 | 首次使用时 |
示例:
// ClassNotFoundException:类文件不存在
try {
Class<?> clazz = loader.loadClass("NonExistentClass");
} catch (ClassNotFoundException e) {
// 类文件不存在
}
// NoClassDefFoundError:类存在但链接失败
public class Main {
public static void main(String[] args) {
try {
MyClass obj = new MyClass(); // MyClass的依赖类不存在
} catch (NoClassDefFoundError e) {
// 类存在但链接失败
}
}
}
解决方案
解决方案:
-
检查依赖:
- 确保所有依赖类都存在
- 检查ProGuard规则是否保留了依赖类
-
检查初始化:
- 确保类的静态初始化不会失败
- 检查静态代码块中的代码
-
检查类路径:
- 确保所有DEX文件都在类路径中
- 启用MultiDex支持
排查方法
排查步骤:
-
查看错误堆栈:
catch (NoClassDefFoundError e) { Log.e("Error", "NoClassDefFoundError", e); e.printStackTrace(); } -
检查依赖类:
// 检查依赖类是否存在 try { Class<?> depClass = Class.forName("com.example.DependencyClass"); } catch (ClassNotFoundException e) { Log.e("Error", "Dependency class not found"); } -
检查初始化:
// 检查类的静态初始化 try { Class<?> clazz = Class.forName("com.example.MyClass"); } catch (ExceptionInInitializerError e) { Log.e("Error", "Class initialization failed", e); }
Android特有的原因
Android特有的原因:
-
MultiDEX问题:
- 依赖类在另一个DEX中
- 需要正确配置MultiDex
-
ProGuard问题:
- 依赖类被ProGuard移除
- 需要在规则中保留
-
AAR依赖问题:
- AAR中的类没有正确打包
- 检查AAR的依赖配置
8.3 LinkageError(Android)
产生原因
LinkageError的原因:
- 类加载过程中出现链接错误
- 通常是版本不兼容或类定义冲突
常见类型:
- NoClassDefFoundError:类定义找不到
- ClassFormatError:类格式错误
- UnsupportedClassVersionError:不支持的类版本
- VerifyError:验证失败
常见类型
1. ClassFormatError:
// DEX文件格式错误
try {
Class<?> clazz = loader.loadClass("com.example.MyClass");
} catch (ClassFormatError e) {
// DEX文件损坏或格式错误
}
2. UnsupportedClassVersionError:
// 类版本不支持(JVM)
// Android中较少见,因为都使用DEX格式
3. VerifyError:
// 字节码验证失败
try {
Class<?> clazz = loader.loadClass("com.example.MyClass");
} catch (VerifyError e) {
// 字节码验证失败
}
解决方案
解决方案:
-
检查DEX文件:
- 确保DEX文件完整
- 重新生成DEX文件
-
检查版本兼容性:
- 确保编译版本和运行版本兼容
- 检查Android版本要求
-
检查字节码:
- 确保字节码正确
- 重新编译
Android特有的原因
Android特有的原因:
-
DEX文件损坏:
- 下载或传输过程中损坏
- 需要重新下载
-
DEX优化失败:
- 优化过程中出错
- 清除优化文件重新优化
-
版本不兼容:
- DEX文件使用了不支持的特性
- 检查Android版本要求
8.4 类加载的性能问题(Android)
类加载的性能开销
类加载的开销:
- I/O开销:读取DEX文件
- 解析开销:解析DEX格式
- 验证开销:验证字节码
- 初始化开销:执行静态初始化
性能影响:
- 首次加载类时开销较大
- 大量类加载可能导致启动慢
- 动态加载类时可能有明显延迟
如何优化类加载(Android)
优化方法:
-
预加载常用类:
// 在应用启动时预加载常用类 public class PreloadManager { public static void preloadClasses() { String[] classes = { "com.example.MainActivity", "com.example.CommonUtils", // ... }; for (String className : classes) { try { Class.forName(className); } catch (ClassNotFoundException e) { // 忽略 } } } } -
减少不必要的类加载:
- 按需加载,不要一次性加载所有类
- 使用懒加载模式
-
优化DEX文件:
- 使用ProGuard优化
- 减少DEX文件大小
-
使用缓存:
// 缓存Class对象 private Map<String, Class<?>> classCache = new HashMap<>(); public Class<?> getClass(String className) { Class<?> clazz = classCache.get(className); if (clazz == null) { clazz = loader.loadClass(className); classCache.put(className, clazz); } return clazz; }
类加载的监控
监控方法:
public class ClassLoadMonitor {
private static Map<String, Long> loadTimes = new HashMap<>();
public static Class<?> loadWithMonitor(ClassLoader loader, String className) {
long startTime = System.currentTimeMillis();
try {
Class<?> clazz = loader.loadClass(className);
long duration = System.currentTimeMillis() - startTime;
loadTimes.put(className, duration);
if (duration > 100) { // 超过100ms记录警告
Log.w("ClassLoad", "Slow class load: " + className + " took " + duration + "ms");
}
return clazz;
} catch (ClassNotFoundException e) {
Log.e("ClassLoad", "Failed to load: " + className, e);
return null;
}
}
public static void printStatistics() {
for (Map.Entry<String, Long> entry : loadTimes.entrySet()) {
Log.d("ClassLoad", entry.getKey() + ": " + entry.getValue() + "ms");
}
}
}
Android AOT编译对类加载的影响
AOT编译的影响:
-
启动速度:
- AOT编译的类加载更快
- 不需要解释执行字节码
-
安装时间:
- 安装时需要编译,时间更长
- 但运行时性能更好
-
存储空间:
- 需要存储.oat文件
- 占用更多存储空间
-
类加载流程:
需要类 ↓ 检查是否有.oat文件 ↓ 有:直接加载.oat文件(更快) 无:从DEX加载(正常速度)
9. Android类加载优化
9.1 类加载的缓存机制(Android)
类加载器内部会缓存已加载的类,避免重复加载。Android中的缓存机制与JVM类似,但针对DEX文件进行了优化。
9.2 多DEX优化
多DEX文件会影响类加载性能。优化方法:
- 合理拆分DEX,将常用类放在主DEX
- 使用ProGuard减少类数量
- 启用MultiDex优化
9.3 类加载的预加载(Android)
在应用启动时预加载关键类,可以减少首次使用时的延迟。
10. Android插件化开发
10.1 插件化的概念
插件化允许在不重新发布应用的情况下,动态加载功能模块。
10.2 插件化的实现原理
核心是使用DexClassLoader加载插件APK中的DEX文件,并通过反射机制调用插件中的类和方法。
10.3 插件化的实践
常见框架:VirtualAPK、RePlugin、Shadow等。
11. Android热修复
11.1 热修复的概念
热修复允许在不重新发布应用的情况下修复线上Bug。
11.2 热修复的实现原理
加载补丁DEX,替换有问题的类。常见方案:类替换、方法替换。
11.3 热修复的实践
常见框架:Tinker、AndFix、Robust等。
12. Android动态加载
12.1 动态加载的概念
在运行时动态加载DEX文件并执行其中的代码。
12.2 动态加载的实现
使用DexClassLoader或InMemoryDexClassLoader加载DEX文件。
12.3 动态加载的实践
注意权限、安全性、性能优化等问题。
第四部分:Android类加载调试与排查
13. Android类加载问题排查
13.1 类加载问题排查方法
ClassNotFoundException排查:
- 检查类名是否正确
- 检查DEX文件是否存在
- 检查类是否在DEX中
- 检查类加载器是否正确
NoClassDefFoundError排查:
- 检查依赖类是否存在
- 检查类的初始化是否失败
- 检查MultiDex配置
13.2 类加载调试技巧(Android)
使用logcat查看类加载日志:
adb logcat | grep -i "classloader\|classnotfound"
使用ClassLoader打印:
ClassLoader loader = getClassLoader();
Log.d("Debug", "ClassLoader: " + loader);
Log.d("Debug", "Parent: " + loader.getParent());
使用Android Studio工具分析:
- 使用Profiler查看类加载情况
- 使用调试器断点调试类加载过程
使用adb命令查看DEX信息:
adb shell dex2oat --dex-file=/data/app/.../classes.dex
13.3 类加载监控(Android)
监控类加载数量:
// 统计已加载的类
int loadedClassCount = 0;
// 使用反射获取已加载的类数量(需要系统权限)
监控类加载时间:
long startTime = System.currentTimeMillis();
Class<?> clazz = loader.loadClass(className);
long duration = System.currentTimeMillis() - startTime;
Log.d("Monitor", "Load time: " + duration + "ms");
使用Android Studio Profiler监控:
- 打开Profiler
- 选择Memory Profiler
- 查看类的加载和卸载情况
第五部分:面试题
14. Java类加载机制面试题(Android版)
14.1 基础概念题
1. 什么是类加载?
完整答案:
类加载是Java虚拟机(在Android中是ART运行时)将类的字节码文件从存储介质(如DEX文件)读取到内存,并转换为虚拟机可以执行的格式的过程。
类加载包含以下步骤:
- 加载(Loading):读取DEX文件,创建Class对象
- 验证(Verification):验证字节码的正确性
- 准备(Preparation):为静态变量分配内存,设置默认值
- 解析(Resolution):将符号引用转换为直接引用
- 初始化(Initialization):执行静态代码块和静态变量赋值
Android中的特点:
- 从DEX文件加载,而不是.class文件
- 支持多DEX文件
- AOT编译可以提前优化
2. 类加载的过程有哪些步骤?
完整答案:
类加载包含5个步骤:
-
加载(Loading):
- 通过类的全限定名获取DEX文件中的字节流
- 将字节流转换为方法区的运行时数据结构
- 在内存中生成Class对象
-
验证(Verification):
- 文件格式验证:DEX文件格式是否正确
- 元数据验证:类的继承关系、字段、方法是否正确
- 字节码验证:方法体中的字节码是否合法
- 符号引用验证:引用的类、字段、方法是否存在
-
准备(Preparation):
- 为类变量(静态变量)分配内存
- 设置类变量的默认初始值(零值)
-
解析(Resolution):
- 将常量池中的符号引用替换为直接引用
- 包括类、字段、方法的解析
-
初始化(Initialization):
- 执行类的初始化代码(静态代码块)
- 为类变量赋值
3. Android中有哪些类加载器?
完整答案:
Android中有以下几种类加载器:
-
BootClassLoader(根类加载器):
- 加载Android系统框架类
- 用C++实现,Java代码中无法直接访问
- 是所有类加载器的父类
-
PathClassLoader(应用类加载器):
- 加载应用APK中的类
- 系统为每个应用创建一个PathClassLoader实例
- 继承自BaseDexClassLoader
-
DexClassLoader(动态加载类加载器):
- 用于动态加载DEX文件
- 支持从任意路径加载DEX
- 常用于插件化和热修复
- 继承自BaseDexClassLoader
-
InMemoryDexClassLoader(内存类加载器,Android 8.0+):
- 从内存中加载DEX文件
- 不需要将DEX文件写入磁盘
- 适合安全敏感的场景
4. 什么是双亲委派模型?
完整答案:
双亲委派模型是类加载器的一种工作模式,核心思想是:当一个类加载器需要加载类时,先不自己加载,而是委托给父类加载器;只有当父类加载器无法加载时,才自己尝试加载。
工作流程:
- 收到加载类的请求
- 检查这个类是否已经加载过
- 如果没有加载过,委托给父类加载器
- 父类加载器也执行相同流程
- 如果所有父类加载器都无法加载,才由当前加载器加载
优点:
- 安全性:防止核心类被替换
- 避免重复加载:同一个类只会被加载一次
- 统一性:保证类的唯一性
Android中的实现:
BootClassLoader
↓
PathClassLoader
↓
DexClassLoader
14.2 深入理解题
1. Android中的PathClassLoader和DexClassLoader有什么区别?
完整答案:
相同点:
- 都继承自BaseDexClassLoader
- 都用于加载DEX文件
- 都支持多DEX文件
不同点:
| 特性 | PathClassLoader | DexClassLoader |
|---|---|---|
| 用途 | 加载已安装APK中的类 | 动态加载未安装APK中的DEX |
| DEX路径 | APK路径 | 任意路径(SD卡、网络等) |
| 创建时机 | 应用启动时系统自动创建 | 开发者手动创建 |
| 使用场景 | 正常应用类加载 | 插件化、热修复、动态加载 |
代码示例:
// PathClassLoader:系统自动创建
ClassLoader pathLoader = getClassLoader(); // PathClassLoader
// DexClassLoader:手动创建
DexClassLoader dexLoader = new DexClassLoader(
"/sdcard/plugin.dex",
getCacheDir().getAbsolutePath(),
null,
getClassLoader()
);
注意:从Android 8.0开始,两者功能基本相同,主要区别在于API设计。
2. 为什么要破坏双亲委派模型(Android场景)?
完整答案:
在Android开发中,以下场景需要破坏双亲委派模型:
-
插件化开发:
- 插件中的类需要与主应用隔离
- 插件可能需要使用不同版本的同一个类库
- 如果遵循双亲委派,插件类会被父类加载器加载,无法实现隔离
-
热修复:
- 需要替换已有的类
- 如果遵循双亲委派,修复后的类会被父类加载器中的原类覆盖
- 需要优先加载补丁中的类
-
动态加载:
- 需要从非标准位置加载类
- 需要控制类的加载顺序和优先级
实现方式:
- 重写loadClass()方法
- 优先从当前加载器加载特定包中的类
- 其他类仍遵循双亲委派
3. 如何实现自定义类加载器(Android)?
完整答案:
在Android中实现自定义类加载器:
-
继承BaseDexClassLoader或DexClassLoader:
public class MyClassLoader extends BaseDexClassLoader { public MyClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, optimizedDirectory, librarySearchPath, parent); } } -
重写findClass()方法(可选):
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 添加自定义逻辑(日志、监控等) Log.d("MyLoader", "Loading: " + name); return super.findClass(name); } -
重写loadClass()方法(破坏双亲委派时):
@Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 自定义加载逻辑 if (name.startsWith("com.plugin.")) { // 优先从当前加载器加载 return findClass(name); } // 其他类委托给父类 return super.loadClass(name, resolve); }
14.3 Android实践题
1. Android中如何实现插件化?
完整答案:
Android插件化的核心是使用DexClassLoader加载插件APK中的DEX文件。基本步骤:
-
加载插件APK:
// 提取插件APK中的DEX文件 String dexPath = extractDexFromApk(pluginApkPath); // 创建DexClassLoader DexClassLoader pluginLoader = new DexClassLoader( dexPath, getOptimizedDir(), null, getClassLoader() ); -
加载插件类:
Class<?> pluginClass = pluginLoader.loadClass("com.plugin.PluginActivity"); -
调用插件方法:
Object plugin = pluginClass.newInstance(); Method method = pluginClass.getMethod("execute"); method.invoke(plugin); -
资源加载:
- 使用AssetManager加载插件资源
- 通过反射创建Resource对象
-
Activity启动:
- 使用代理Activity机制
- 或Hook ActivityManagerService(需要系统权限)
注意事项:
- 插件需要独立编译
- 需要处理资源冲突
- 需要处理四大组件的启动
- 需要考虑版本兼容性
15. 综合面试题(Android版)
15.1 综合理解题
1. Android类加载与JVM类加载的区别?
完整答案:
主要区别:
| 特性 | JVM | Android |
|---|---|---|
| 字节码格式 | .class文件 | .dex文件 |
| 类加载器 | Bootstrap/Extension/App | BootClassLoader/PathClassLoader/DexClassLoader |
| 多文件支持 | 多个.jar文件 | 多个.dex文件 |
| 编译方式 | JIT(即时编译) | AOT(提前编译)+ JIT |
| 扩展类加载器 | 有 | 无 |
| 类文件组织 | classpath下的jar文件 | APK中的DEX文件 |
详细对比:
-
字节码格式:
- JVM:每个类一个.class文件
- Android:多个类合并成一个DEX文件
-
类加载器层次:
- JVM:Bootstrap → Extension → Application → Custom
- Android:BootClassLoader → PathClassLoader → DexClassLoader
-
优化策略:
- JVM:主要在运行时优化(JIT)
- Android:安装时优化(AOT)+ 运行时优化(JIT)
16. 高级面试题(Android版)
16.1 深入原理题
1. Android类加载器的底层实现原理?
完整答案:
Android类加载器的底层实现:
-
BaseDexClassLoader:
- 包含DexPathList对象
- DexPathList维护DEX文件路径列表
- 通过DexPathList查找和加载类
-
DexPathList:
- 包含Element数组,每个Element对应一个DEX文件
- 查找类时遍历所有Element
- 找到后使用DexFile加载类
-
DexFile:
- 封装了DEX文件的读取和解析
- 通过JNI调用底层C++代码
- 最终在ART运行时中完成类的加载
加载流程:
loadClass()
↓
findClass()
↓
DexPathList.findClass()
↓
DexFile.loadClass()
↓
ART运行时加载类
本文档完
本文档涵盖了Android平台Java类加载机制的核心知识点,包括理论基础、类加载器、实践应用、调试排查和面试题。内容系统全面,适合Android开发人员学习和面试准备。