动态加载和类加载机制

参考:Android加壳脱壳学习(1)——动态加载和类加载机制详解-看雪

类加载器

Android中的类加载器机制与JVM一样遵循双亲委派模式

双亲委派模式

双亲委派模式定义

通俗来讲就是,加载.class文件是看自己这个“类加载器”有没有加载过,
没有就找上一级加载,
上一级没有就找上上级加载,
上上级没有就找上上上级加载,

如果最终上上上级还是发现没有加载过,那这个时候就要加载了。

此时上上上级 使用findClass(name) 尝试加载,
如果上上上级加载失败就叫上上级 使用findClass(name) 尝试加载,
如果上上级加载失败就叫上级 使用findClass(name) 尝试加载。

  • 加载.class文件时,以递归的形式逐级向上委托给父加载器ParentClassLoader加载,如果加载过了,就不用再加载一遍
  • 如果父加载器没有加载过,继续委托给父加载器去加载,一直到这条链路的顶级,顶级ClassLoader如果没有加载过,则尝试加载,加载失败,则逐级向下交还调用者加载

双亲委派的作用
(1) 防止同一个.class文件重复加载
(2) 对于任意一个类确保在虚拟机中的唯一性。由加载它的类加载器和这个类的全类名一同确立其在Java虚拟机中的唯一性
(3) 保证.class文件不被篡改,通过委派方式可以保证系统类的加载逻辑不被篡改

|260 |317

Android中类加载机制

总结来讲:
核心就是Zygote进程,在解析了init.rc后会启动app_process进程,随后该进程调用ZygoteInit.main( )初始化Zygote。
AndroidRuntime.start()调用startvm创建Dalvik虚拟机,startreg注册JNI函数。建立socket通道。预加载类与资源。fork system_server进程。等待创建新进程的请求。

回顾Dalvik虚拟机 我们执行app_process程序,进入main函数里面
,然后进行AndroidRuntime::start
|407
|403
Zygote启动 然后进入Java层
|650
|641

Zygote总结:
(1)解析init.zygote.rc中的参数,创建AppRuntime并调用AppRuntime.start()方法
(2)调用AndroidRuntime的startVM()方法创建虚拟机,再调用startReg()注册JNI函数
(3)通过JNI方式调用ZygoteInit.main()第一次进入Java世界
(4)registerZygoteSocket()建立socket通道,zygote作为通信的服务端,用于响应客户端请求
(5)preload()预加载通用类、drawable和color资源、openGL以及共享库以及WebView,用于提高app启动效率
(6)通过startSystemServer()fork system_server进程,也是Java Framework的运行载体(下面讲到system server再详细讲解)
(7)调用runSelectLoop(),随时待命,当接收到请求创建新进程请求时立即唤醒并执行相应工作

Android的类加载机制和JVM一样遵循双亲委派模式,在dalvik/art启动时将所有Java基本类Android系统框架基本类加载进来,预加载的类记录在/frameworks/base/config/preloaded-classes中。
因为所有进程都是通过Zygote fork出来的,Zygote在启动时加载了,后续所有进程在被fork出来时都会带有这些基本类。

类方法扫盲

接下来会提到一些加载类的方法,这里提前介绍下:
首先,博客里所有[Ff]or[Nn]ame都是forName,Java采用驼峰命名法,不存在其他错误的写法。

Q1: forName()loadClass()findClass() 的区别

  1. Class.forName()
  • 定义与作用
    Class.forName() 用于动态加载指定全限定名的类到 JVM,并返回对应的 Class 对象
    • 默认情况下会触发类的初始化(即执行静态代码块)
    • 提供重载方法 forName(String name, boolean initialize, ClassLoader loader),可控制是否初始化类及指定类加载器
  • new 的区别
    new 是静态绑定,直接实例化对象;forName() 是动态加载,需通过反射(如 newInstance())创建对象
  1. ClassLoader.loadClass()
  • 定义与作用
    loadClass() 是类加载的核心方法,实现了双亲委派模型
    1. 检查类是否已加载
    2. 委派父类加载器加载
    3. 若父类无法加载,调用 findClass() 加载
      默认不初始化类,需显式调用 resolveClass() 或实例化对象时才会触发静态代码块
  • findClass() 的关系
    loadClass() 负责加载流程控制,findClass() 提供具体加载实现开发者通常覆写 findClass() 而非 loadClass(),以避免破坏双亲委派机制
  1. ClassLoader.findClass()
  • 定义与作用
    findClass()ClassLoader 类的受保护方法,需子类重写,用于根据类名查找并加载字节码
  • 应用场景
    自定义类加载器时,通过覆写 findClass() 实现从非标准路径加载类(如加密类文件、热部署等)

Q2:他们不都是加载类吗?

类加载的生命周期分为:

  • 加载(将 .class 字节码读入内存,生成 Class 对象)、
  • 链接(1.验证字节码合法性、2.为静态变量分配内存并赋默认值,如 int 初始化为 0、3.将符号引用转换为直接引用)、
  • 初始化(执行 <clinit> 方法,为静态变量赋真实值,执行静态代码块)。
方法 作用阶段 是否触发初始化 作用
ClassLoader.loadClass() 加载 + 部分链接 默认否(需手动) 管理双亲委派流程,确保类加载符合 JVM 规范
ClassLoader.findClass() 仅完成 加载 仅实现从自定义来源(如加密文件、网络)读取字节码,不处理后续流程。
Class.forName() 加载 + 链接 + 初始化 是(默认) 封装了整个加载流程,直接交付可用的类

Class.forName 和 ClassLoader.loadClass加载有何不同:

  • ClassLoader.loadClass也能加载一个类,但是不会触发类的初始化(也就是说不会对类的静态变量,静态代码块进行初始化操作)
  • Class.forName这种方式,不但会加载一个类,还会触发类的初始化阶段,也能够为这个类的静态变量,静态代码块进行初始化操作

Q3:ClassClassLoader 的区别

  1. Class
  • 功能
    表示 JVM 中的类和接口,提供反射能力(如获取字段、方法、构造器等)
  • 关键方法
    forName()(动态加载类)、newInstance()(创建实例)、getDeclaredFields()(反射操作)等
  • 与类加载的关系
    Class.forName() 内部通过 ClassLoader 实现加载,但会触发类的初始化
  1. ClassLoader
  • 功能
    抽象类加载器,负责从不同来源(如文件、网络)加载类的字节码,生成 Class 对象
  • 关键方法
    loadClass()(加载流程控制)、findClass()(自定义加载逻辑)、defineClass()(字节码转 Class 对象)
  • 特点
    • 遵循双亲委派模型,避免重复加载
    • 默认不初始化类,需显式调用 newInstance() 才会触发静态代码块

结合上面的方法来理解的话,就是Class类使用forName方法包含了整个类加载,在这个过程中会调用ClassLoader的加载功能,ClassLoader是专门实现类加载的部分的,但是Class类还能实现其他的一些功能,比如反射、类型检查、创建实例等。

Q4:什么时候会加载类

Answer:
1.隐式加载时:

  • 创建类的实例,也就是new一个对象
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射Class.forName("android.app.ActivityThread")
  • 初始化一个类的子类(会首先初始化子类的父类)
    2.显式加载时:
  • 使用LoadClass()加载
  • 使用forName()加载

Android类加载器层级关系及分析

|615
|977
类加载器 继承关系 功能 加载目标 optimizedDirectory 典型应用场景 引入版本
BootClassLoader ClassLoader的内部类 预加载Android系统核心类(如ActivityContext等) /system/framework下的核心类 系统启动预加载 所有Android版本
PathClassLoader BaseDexClassLoader 加载已安装APK的类/data/app目录)或系统类 已安装的APK文件 系统默认路径(/data/dalvik-cache 默认应用类加载 所有Android版本
DexClassLoader BaseDexClassLoader 动态加载未安装的APK/JAR/DEX文件 任意路径的APK、JAR、DEX文件 需指定(应用私有目录) 插件化、热修复 所有Android版本
InMemoryDexClassLoader BaseDexClassLoader 直接从内存加载DEX数据(无需写入磁盘) 内存中的DEX数据(ByteBuffer形式) 动态生成代码的安全加载 Android 8.0+ (API 26)
DelegateLastClassLoader BaseDexClassLoader 修改双亲委托机制:优先自行加载类,失败后再委托父加载器 任意路径的类文件 需指定 覆盖系统类行为(如动态补丁) Android 8.1+ (API 27)

<1> BootClassLoader

BootClassLoader 是启动类加载器,用于加载 Zygote 进程在缓存中已经预加载的基本类。当在缓存中无法获取到类时,BootClassLoader 会直接调用自己的 findClass方法来加载类,而调用 findClass方法实际上就是调用了Class.classforName方法

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    return Class.classForName(name, false, null);
}

由于它是基类 ClassLoader 的一个内部类,具有包访问权限,应用程序无法直接访问 BootClassLoader。

ZygoteInit.preloadClasses() 方法中,通过指定 BootClassLoader 作为类加载器使用 Class.forName() 加载基本类,该方法触发类初始化,此后应用程序就可以通过 Class.forName()来加载这些已经预加载的基本类。

上图中体现了ZygoteInit.preload()方法使用的是Bootloader,而无论是系统类加载器(PathClassLoader)还是自定义的类加载器(DexClassLoader),最顶层的祖先加载器默认是 BootClassLoader,与 JVM 一样,保证了基本类的类型安全

<2> PathClassLoader

加载已安装apk的类和系统类
PathClassLoader 是作为应用程序的系统类加载器,也是在 Zygote 进程启动的时候初始化的
基本流程为:Zygote启动 → 预加载系统资源 → 创建SystemServer进程时初始化PathClassLoader → 应用进程fork继承类加载器 → 默认使用PathClassLoader加载已安装APK的dex。
所以每一个 APP 进程从 Zygote 中 fork 出来之后都自动携带了一个 PathClassLoader,它通常用于加载 已安装的 apk 里面的 .dex 文件

public class PathClassLoader extends BaseDexClassLoader {
 
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
 
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

<3> DexClassLoader

可以从包含classes.dex的jar或者apk中,加载类的类加载器, 可用于执行动态加载, 但必须是app私有可写目录来缓存odex文件. 能够加载系统没有安装的apk或者jar文件, 因此很多热修复和插件化方案都是采用DexClassLoader

public class
DexClassLoader extends BaseDexClassLoader {
   public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

总结:
我们可以发现DexClassLoader与PathClassLoader都继承于BaseDexClassLoader,这两个类只是提供了自己的构造函数,没有额外的实现
区别:
DexClassLoader提供了optimizedDirectory,而PathClassLoader则没有,optimizedDirectory正是用来存放odex文件的地方,所以可以利用DexClassLoader实现动态加载

<4> BaseDexClassLoader

BaseDexClassLoader派生出两个子类加载器:PathClassLoaderDexClassLoader

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;  //记录dex文件路径信息
/*
* 构造函数 初始化 DexPathList,用来查找Dex、SO库的路径
* dexPath: 包含目标类或资源的apk/jar列表;当有多个路径则采用:分割;
* optimizedDirectory: 优化后dex文件存在的目录, 可以为null;
* libraryPath: native库所在路径列表;当有多个路径则采用:分割;
* ClassLoader:父类的类加载器
*/
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }


上图中可以看出,BaseDexClassLoader的相关流程:

  • 根据 传参dexPath 确定Dex文件路径
  • 找到了就加载Dex,根据optimizedDirectory(odex的存放目录)调用不同的DexClassLoader/PathClassLoader来加载Dex文件
  • 执行 openDexFileNative(oat同理)加载odex
    • 解析 dexPath 中的 DEX 文件路径
    • 检查 optimizedDir 目录下是否存在有效的 ODEX 文件
      • ODEX 有效:直接加载现有的 ODEX 文件。
      • ODEX 无效或不存在:触发优化流程,调用dexopt。
        • 调用 dexopt
          • 验证 DEX 文件的合法性(如签名、字节码结构)
          • 优化 DEX 为 ODEX(指令替换、字节码对齐等)。
          • 将 ODEX 写入 optimizedDirectory 目录
    • 加载 ODEX
      • openDexFileNative 将生成的 ODEX 文件通过 mmap 映射到内存。
      • 返回一个 mCookie(整型句柄),供后续类查找使用。
posted @ 2025-05-12 17:07  方北七  阅读(81)  评论(0)    收藏  举报