Android 插件化核心:ClassLoader 机制与原理详解
Java 代码都是写在 Class 里面的,程序运行在虚拟机上时,虚拟机需要把需要的 Class 加载进来才能创建实例对象并工作,而完成这一个加载工作的角色就是 ClassLoader。
Android ClassLoader 负责将类加载到虚拟机内存中,支持动态加载 dex 文件。文章解析了 BootClassLoader 与 PathClassLoader/DexClassLoader 的区别,阐述了双亲委派模型的工作流程及限制。重点讨论了动态加载时面临的组件注册缺失和资源 ID 不匹配问题,并分析了优化目录对性能的影响。

Java 代码都是写在 Class 里面的,程序运行在虚拟机上时,虚拟机需要把需要的 Class 加载进来才能创建实例对象并工作,而完成这一个加载工作的角色就是 ClassLoader。
Android 的 Dalvik/ART 虚拟机如同标准 Java 的 JVM 虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此,我们可以利用这一点,在程序运行时手动加载 Class,从而达到代码动态加载可执行文件的目的。Android 的 Dalvik/ART 虚拟机虽然与标准 Java 的 JVM 虚拟机不一样,ClassLoader 具体的加载细节不一样,但是工作机制是类似的,也就是说在 Android 中同样可以采用类似的动态加载插件的功能,只是在 Android 应用中动态加载一个插件的工作要比 Eclipse 加载一个插件复杂许多。
动态加载的基础是 ClassLoader,从名字也可以看出,ClassLoader 就是专门用来处理类加载工作的,所以这货也叫类加载器,而且一个运行中的 APP 不仅只有一个类加载器。
其实,在 Android 系统启动的时候会创建一个 Boot 类型的 ClassLoader 实例,用于加载一些系统 Framework 层级需要的类,我们的 Android 应用里也需要用到一些系统的类,所以 APP 启动的时候也会把这个 Boot 类型的 ClassLoader 传进来。
此外,APP 也有自己的类,这些类保存在 APK 的 dex 文件里面,所以 APP 启动的时候,也会创建一个自己的 ClassLoader 实例,用于加载自己 dex 文件中的类。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
Log.i(TAG, "[onCreate] classLoader : " + classLoader.toString());
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
Log.i(TAG, "[onCreate] parent classLoader : " + classLoader.toString());
}
}
}
运行日志通常如下所示:
[onCreate] classLoader : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/me.kaede.anroidclassloadersample-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
[onCreate] parent classLoader : java.lang.BootClassLoader@14af4e32
可以看见有 2 个 ClassLoader 实例,一个是 BootClassLoader(系统启动的时候创建的),另一个是 PathClassLoader(应用启动时创建的,用于加载'/data/app/me.kaede.anroidclassloadersample-1/base.apk'里面的类)。由此也可以看出,一个运行的 Android 应用至少有 2 个 ClassLoader。
动态加载外部的 dex 文件的时候,我们也可以使用自己创建的 ClassLoader 实例来加载 dex 里面的 Class,不过 ClassLoader 的创建方式有点特殊,我们先看看它的构造方法。
/*
* constructor for the BootClassLoader which needs parent to be null.
*/
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
创建一个 ClassLoader 实例的时候,需要使用一个现有的 ClassLoader 实例作为新创建的实例的 Parent。这样一来,一个 Android 应用,甚至整个 Android 系统里所有的 ClassLoader 实例都会被一棵树关联起来,这也是 ClassLoader 的**双亲代理模型(Parent-Delegation Model)**的特点。
JVM 中 ClassLoader 通过 defineClass 方法加载 jar 里面的 Class,而 Android 中这个方法被弃用了。
@Deprecated
protected final Class<?> defineClass(byte[] classRep, int offset, int length) throws ClassFormatError {
throw new UnsupportedOperationException("can't load this type of class file");
}
取而代之的是 loadClass 方法。
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
从源码中我们也可以看出,loadClass 方法在加载一个类的实例的时候:
这样做有个明显的特点,如果一个类被位于树根的 ClassLoader 加载过,那么在以后整个系统的生命周期内,这个类永远不会被重新加载。这意味着一旦某个类被系统框架层加载,应用层无法覆盖它,保证了系统稳定性。
如果你希望通过动态加载的方式,加载一个新版本的 dex 文件,使用里面的新类替换原有的旧类,从而修复原有类的 BUG,那么你必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么 ClassLoader 会一直优先使用旧类。
如果旧类总是优先于新类被加载,我们也可以使用一个与加载旧类的 ClassLoader 没有树的继承关系的另一个 ClassLoader 来加载新类,因为 ClassLoader 只会检查其 Parent 有没有加载过当前要加载的类。如果两个 ClassLoader 没有继承关系,那么旧类和新类都能被加载。不过这样一来又有另一个问题了,在 Java 中,只有当两个实例的类名、包名以及加载其的 ClassLoader 都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的 ClassLoader 不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。
核心规则: 同一个 Class = 相同的 ClassName + PackageName + ClassLoader
这个在采用动态加载功能的开发中容易出现,请注意。
平时开发的时候,使用 DexClassLoader 就够用了,但是我们不妨挖一下这两者具体细节上的区别。
class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
这两者只是简单的对 BaseDexClassLoader 做了一下封装,具体的实现还是在父类里。不过这里也可以看出,PathClassLoader 的 optimizedDirectory 只能是 null,进去 BaseDexClassLoader 看看这个参数是干什么的。
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
这里创建了一个 DexPathList 实例,进去看看。
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
// ...省略部分初始化代码
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) {
zip = new ZipFile(file);
}
// ...
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
/**
* Converts a dex/jar file path and an output directory to an
* output file path for an associated optimized dex file.
*/
private static String optimizedPathFor(File path, File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
看到这里我们明白了,optimizedDirectory 是用来缓存我们需要加载的 dex 文件的,并创建一个 DexFile 对象,如果它为 null,那么会直接使用 dex 文件原有的路径来创建 DexFile 对象。 optimizedDirectory 必须是一个内部存储路径,还记得我们之前说过的,无论哪种动态加载,加载的可执行文件一定要存放在内部存储。DexClassLoader 可以指定自己的 optimizedDirectory,所以它可以加载外部的 dex,因为这个 dex 会被复制到内部路径的 optimizedDirectory;而 PathClassLoader 没有 optimizedDirectory,所以它只能加载内部的 dex,这些大都是存在系统中已经安装过的 apk 里面的。
上面还只是创建了类加载器的实例,其中创建了一个 DexFile 实例,用来保存 dex 文件,我们猜想这个实例就是用来加载类的。Android 中,ClassLoader 用 loadClass 方法来加载我们需要的类。
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
loadClass 方法调用了 findClass 方法,而 BaseDexClassLoader 重载了这个方法,得到 BaseDexClassLoader 看看。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
结果还是调用了 DexPathList 的 findClass。
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
这里遍历了之前所有的 DexFile 实例,其实也就是遍历了所有加载过的 dex 文件,再调用 loadClassBinaryName 方法一个个尝试能不能加载想要的类,真是简单粗暴。
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
看到这里想必大家都明白了,loadClassBinaryName 中调用了 Native 方法 defineClass 加载类。
至此,ClassLoader 的创建和加载类的过程的完成了。有趣的是,标准 JVM 中,ClassLoader 是用 defineClass 加载类的,而 Android 中 defineClass 被弃用了,改用了 loadClass 方法,而且加载类的过程也挪到了 DexFile 中,在 DexFile 中加载类的具体方法也叫 defineClass,不知道是 Google 故意写成这样的还是巧合。
通过上面的分析,我们知道使用 ClassLoader 动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的 Java 程序,在 Android 程序中使用动态加载主要有两个麻烦的问题:
Android 中许多组件类(如 Activity、Service 等)是需要在 Manifest 文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作。这是因为 Android 系统在应用安装时会解析 AndroidManifest.xml 文件,将其内容写入系统数据库。动态加载的类并没有经过安装流程,因此系统并不知道它们的存在。
Res 资源是 Android 开发中经常用到的,而 Android 是把这些资源用对应的 R.id 注册好,运行时通过这些 ID 从 Resource 实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到 R.id 的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源 ID 根本和现有的 Resource 实例中保存的资源 ID 对不上。
R.java 是在编译期根据资源文件生成的,动态加载的 APK 拥有独立的资源表,但宿主应用运行时只加载了宿主自身的资源映射。为了解决这个问题,通常需要 Hook 资源加载逻辑,让动态模块能够访问宿主资源或共享资源池。
ClassLoader 是 Android 动态加载技术的基石。理解其双亲委派模型、加载流程以及 DexClassLoader 与 PathClassLoader 的区别,对于开发高性能、高稳定性的插件化方案至关重要。尽管面临组件注册和资源映射的挑战,通过合理的架构设计(如反射、Hook 技术),依然可以实现灵活的动态功能扩展。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online