ClassLoader问题汇总
1、Android中有哪几种ClassLoader?它们的作用和区别是什么?
从源码中可以看到,Android中有三个ClassLoader,分别是BaseDexClassLoader、PathClassLoader、DexClassLoader。

从上图可以看出,ClassLoader的直接子类是BaseDexClassLoader、SecureClassLoader;间接子类是DelegateLastClassLoader、DexClassLoader、InMemoryDexClassLoader、PathClassLoader、URLClassLoader。 我们比较常用的是BaseDexClassLoader、DexClassLoader和PathClassLoader。
BaseDexClassLoader
DexClassLoader和PathClassLoader都继承自BaseDexClassLoader。DexClassLoader和PathClassLoader的主要功能都放在了BaseDexClassLoader中。看一下BaseDexClassLoader的构造函数:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
参数有四个,分别是: String dexPath,指要加载的apk或者jar包的路径。最终要将该dexPath路径上的文件ODEX优化到optimizedDirectory文件夹中,然后再对ODEX后的文件进行加载。 File optimizedDirectory,dexPath路径上的文件需要优化到的文件夹。 String libraryPath,apk或者jar包中的C或者C++库所存放的路径。 ClassLoader parent,该ClassLoader的父类ClassLoader,一般为当前执行类的ClassLoader。
PathClassLoader
PathClassLoader继承自BaseDexClassLoader,看一下它的构造函数:
public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
构造函数直接调用了BaseDexClassLoader的构造函数,注意其中的第二个参数传了null。
DexClassLoader
DexClassLoader继承自BaseDexClassLoader,看一下它的构造函数:
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), library, parent);
}
构造函数直接调用了BaseDexClassLoader的构造函数,我们对比一下PathClassLoader的构造函数,可以看到除了第二个参数不同之外,其他参数都是一样的。 而我们知道PathClassLoader只可以加载Android系统类和应用的类。而DexClassLoader不仅仅可以加载Android系统类和应用的类,并且可以加载外部jar包和Apk包的类。 因此,我们猜想可能跟第二个参数不一致导致的。 再返回到BaseDexClassLoader中看第二个参数的作用,可以看到在BaseDexClassLoader中使用optimizedDirectory构造了DexPathList对象。那么我们来分析一下DexPathList这个类。
DexPathList
首先看到DexPathList的构造函数
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
。。。省略
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
其中optimizedDirectory参数用作了makeDexElements方法的参数,那我们再去看makeDexElements方法。
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)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
这里又调用了loadDexFile方法,
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);
}
}
可以看到当optimizedDirectory为null时,直接new了一个DexFile对象,而不为null时,调用了DexFile的loadDex静态方法。在DexFile的loadDex最终new了一个DexFile对象,并且在DexFile的构造函数中调用了openDexFile方法。该方法中的原型如下:
native private static int openDexFile(String sourceName, String outputName,
int flags) throws IOException;
可以看到PathClassLoader这里应该为null,而DexClassLoader这里应该是dex优化后的路径。所以PathClassLoader没有设置Dex优化后的存放路径。其实optimizedDirectory为null时的默认路径就是/data/dalvik-cache 目录。 所以这就是PathClassLoader只能加载Android系统或者应用的类的原因,而DexClassLoader可以加载外部jar包或者Apk包的原因。
总结
Android中一共有三种ClassLoader,分别是BaseDexClassLoader、PathClassLoader、DexClassLoader。PathClassLoader和DexClassLoader都继承于BaseDexClassLoader,PathClassLoader只能加载Android系统和应用内的类,而DexClassLoader除了可以加载Android系统和应用内的类外,还可以加载外部的apk和jar包。
2、简述双亲委托模型
Android的双亲委托模型就是当某个类加载器接到加载某个类的请求时,其首先会将加载任务委托给其父加载器,以此递归,如果父加载器可以完成加载任务,则返回,否则再使用该加载器进行类的加载。
使用双亲委托模型的优点
1、可以避免类的重复加载。当父加载器已经加载了此类,子类加载器就不用再加载一遍,否则会形成类的重复加载。 2、考虑到安全方面,如果不使用双亲委托模型,那么如果我们自定义一个ClassLoader去加载一个系统类,例如java.lang.Object。如果没有双亲委托模型,那么我们自定义的ClassLoader就会去加载这个类。
Android的类加载是否一定会遵循双亲委托模式
并不是的,双亲委托模式只是JDK提供的ClassLoader类的实现方式。在实际开发中,我们可以通过自定义ClassLoader,并重写父类的loadClass方法,就可以打破这一机制。
3、简述双亲委托模型在热修复领域的应用
热修复目前有三种方案,分别是底层替换方案、类加载方案和Instant Run方案。 而双亲委托模型在热修复上面的应用主要体现在类加载方案上面。所以我们这里重点看一下怎么使用类加载方案来实现热修复。 从上面ClassLoader加载类的过程中,我们知道类是由DexPathList中的findClass方法来加载类的。看一下findClass方法:
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//1
Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Element内部封装了DexFile,DexFile用于加载dex文件,所以dex文件和Element是一一对应的。当有多个dex文件时,就会有多个Element对象,形成Element数组。当需要查找类时,会去遍历这个数组,其实相当于去遍历dex文件数组,然后通过Element的findClass方法去查找类。当查找到相应的类后,就返回,否则继续使用下一个Element来查找。 上面的加载流程其实就是双亲委托模型,所以我们可以将修复后的类patch.class打包成包含dex的补丁包,并将其放在Element数组的第一个位置,这样当去查找该类的时候,就先会去修复后的dex中查找到修复后的类,而后面有问题的类并不会被查找到。这就是ClassLoader的双亲委托模型在热修复中的应用。
转载自:https://juejin.cn/post/6844903944167096327