- 本篇是基于上一篇ClassLoader(一) —— Java ClassLoader。
- Android虚拟机和JVM一样,运行程序时首先要将对应的类加载到内存中。但是和JVM不同的是Android虚拟机上运行的是Dex字节码,因此Android的ClassLoader和Java的ClassLoader有一定不同。
Android 类加载
-
Android中的类加载器有
- BootClassLoader
- URLClassLoader
- PathClassLoader
- DexClassLoader
- BaseDexClassLoader
- ClassLoader
其中BootClassLoader,PathClassLoader和DexClassLoader是重点。
看看他们之间的继承关系:
BootClassLoader
- BootClassLoader在Android系统启动的时候就被创建,它用于加载一些Android系统框架的类,包括APP用到的一些系统类。它是ClassLoader中的内部类,由Java实现。这个内部类是包内可见,所以我们没法使用。
URLClassLoader
- 它继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器。
PathClassLoader
PathClassLoader是用来加载Android系统类和应用的类。
在Dalvik虚拟机上PathClassLoader只能加载已安装的apk的dex文件。但在ART虚拟机上可以加载未安装的apk的dex文件。
-
PathClassLoader的源码,只有2个构造方法:
public 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的构造方法,所以每个参数的含义将会留到BaseDexClassLoader再分析。
DexClassLoader
DexClassLoader可以加载一个未安装的APK,也可以加载其它包含dex文件的JAR/ZIP类型的文件,可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。
上面说dalvik不能直接识别jar,DexClassLoader却可以加载jar文件,这难道不矛盾吗?其实在BaseDexClassLoader里对".jar",".zip",".apk",".dex"后缀的文件最后都会生成一个对应的dex文件,所以最终处理的还是dex文件,而URLClassLoader并没有做类似的处理。
-
DexClassLoader的源码,只有1个构造方法:
public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
由于只是调用了父类BaseDexClassLoader的构造方法,所以每个参数的含义将会留到BaseDexClassLoader再分析。
BaseDexClassLoader
PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,其中的主要逻辑都是在BaseDexClassLoader完成的。
-
先来填下上文留下的坑,看看BaseDexClassLoader的构造方法:
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, null); if (reporter != null) { reporter.report(this.pathList.getDexPaths()); } }
BaseDexClassLoader的构造函数包含四个参数,分别为:
- dexPath:指目标类所在的APK或jar文件的路径,类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径。如果要包含多个路径,路径之间必须使用特定的分割符分隔,分隔符通常为":"。
- optimizedDirectory:由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径。如果该参数为null,则设置默认路径为/data/dalvik-cache 目录。
- libraryPath:指目标类中所使用的C/C++库存放的路径,多个路径也是以“:”分隔。
- parent:父类加载器,遵从双亲委派。
-
在BaseDexClassLoader中的成员变量
private final DexPathList pathList
十分重要,ClassLoader中的抽象方法findClass()
、findResource()
、findResources()
、findLibrary()
均是基于 pathList 来实现的(省略了部分源码):@Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); ... return c; } @Override protected URL findResource(String name) { return pathList.findResource(name); } @Override protected Enumeration<URL> findResources(String name) { return pathList.findResources(name); } @Override public String findLibrary(String name) { return pathList.findLibrary(name); }
那我们来看看DexPathList中做了什么。
DexPathList
-
在DexPathList中有个
private Element[] dexElements
是它的重点,Element是DexPathList的内部类,有下面的成员变量:static class Element { private final File path; private final DexFile dexFile; private ClassPathURLStreamHandler urlHandler; private boolean initialized; }
-
让我们看看Element数组是如果生成的:
//在DexPathList构造方法中调用makeDexElements方法生成 public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { ... //splitDexPath()方法是把String切割成多个地址,再把每个地址生成File,该方法返回List<File> this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); ... }
private static Element[] makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader) { Element[] elements = new Element[files.size()]; int elementsPos = 0; //打开所有文件并预先加载(直接或包含)dex文件 for (File file : files) { if (file.isDirectory()) { // 如果是文件夹,则直接添加 Element,这个一般是用来处理 native 库和资源文件 elements[elementsPos++] = new Element(file); } else if (file.isFile()) { String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) { // 直接是.dex文件,而不是zip/jar文件(apk归为zip),则直接加载dex文件 try { DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements); if (dex != null) { elements[elementsPos++] = new Element(dex, null); } } catch (IOException suppressed) { System.logE("Unable to load dex file: " + file, suppressed); suppressedExceptions.add(suppressed); } } else { //如果是zip/jar文件(apk归为zip),加载dex文件。 DexFile dex = null; try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { suppressedExceptions.add(suppressed); } //如果dex为空则不传进Element,file文件是肯定会传进的 if (dex == null) { elements[elementsPos++] = new Element(file); } else { elements[elementsPos++] = new Element(dex, file); } } } else { System.logW("ClassLoader referenced unknown path: " + file); } } if (elementsPos != elements.length) { elements = Arrays.copyOf(elements, elementsPos); } return elements; }
DexPathList.loadDexFile()
方法最终会调用 JNI 层的方法来读取 dex 文件,这里不再深入探究,有兴趣的可以阅读 从源码分析 Android dexClassLoader 加载机制原理 这篇文章深入了解。 -
获得了Element数组就可以通过DexPathList.findClass()方法来对类进行加载了,源码如下:
public Class<?> findClass(String name, List<Throwable> suppressed) { // 遍历 dexElements 数组,依次寻找对应的 class,一旦找到就终止遍历 for (Element element : dexElements) { Class<?> clazz = element.findClass(name, definingContext, suppressed); if (clazz != null) { return clazz; } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }
这里有关于热修复实现的一个点,就是将补丁 dex 文件放到 dexElements 数组前面,这样在加载 class 时,优先找到补丁包中的 dex 文件,加载到 class 之后就不再寻找,从而原来的 apk 文件中同名的类就不会再使用,从而达到修复的目的。
ClassLoader
-
ClassLoader是所有ClassLoader的最终父类。我们来瞧瞧ClassLoader的源码:
public abstract class ClassLoader { static private class SystemClassLoader { public static ClassLoader loader = ClassLoader.createSystemClassLoader(); } //父加载器 private final ClassLoader parent; private static ClassLoader createSystemClassLoader() { String classPath = System.getProperty("java.class.path", "."); String librarySearchPath = System.getProperty("java.library.path", ""); //可以看出构造PathClassLoader传入了BootClassLoader return new PathClassLoader(classPath, librarySearchPath,BootClassLoader.getInstance()); } public static ClassLoader getSystemClassLoader() { return SystemClassLoader.loader; } private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; } protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); } protected ClassLoader() { //外界没有传入指定父加载器的情况 this(checkCreateClassLoader(), getSystemClassLoader()); } public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ // 检查是否已经加载过 Class<?> c = findLoadedClass(name); if (c == null) { // 没有被加载过 // 首先委派给父类加载器加载 try { if (parent != null) { //父加载器不为空则调用父加载器的loadClass c = parent.loadClass(name, false); } else { //父加载器为空 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // 如果父类加载器无法加载,才尝试加载 c = findClass(name); } } return c; } private Class<?> findBootstrapClassOrNull(String name){ return null; } ... }
从上面可以看出Android中的ClassLoader和Java中的区别并不大,ClassLoader的构造方法也是分为指定parent和不指定parent两种,不同的是在外界不指定parent的情况下,会通过
createSystemClassLoader()
来获取到PathClassLoader作为parent。直白的说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是PathClassLoader,且此PathClassLoader父构造器为BootClassLoader。 可以看到Android中的ClassLoader.loadClass()和Java中的基本是不变的,都是实现了双亲委托。甚至就连在Java中调用BootstrapClassLoader的findBootstrapClassOrNull方法也保留着,然而android中并没有BootstrapClassLoader,而且并没有出现因为某个ClassLoader不是Java实现的而导致无法持有父加载器的情况。。。所以在这里该方法直接返回nuil。
双亲委派
通过从ClassLoader.loadClass()方法中我们可以明白Android ClassLoader中的双亲委派流程。
-
带上DexClassLoader一起玩双亲委派:
ClassLoader的构造方法中有一个参数是parent,那么是不是有办法把PathClassLoader的parent替换成我们想要的DexClassLoader,在把DexClassLoader的parent设置成BootClassLoader,再加上父委托的机制,查找类的过程就变成BootClassLoader->DexClassLoader->PathClassLoader,这样我们就能够通过双亲委派先去加载外部apk的类了。我们可以通过反射来实现我们的设想。
public static void loadApk(Context context, String apkPath) { File dexFile = context.getDir("dex", Context.MODE_PRIVATE); File apkFile = new File(apkPath); //获取到PathClassLoader ClassLoader classLoader = context.getClassLoader(); //创建DexClassLoader并设置父加载器为BootClassLoader DexClassLoader dexClassLoader = new DexClassLoader(apkFile.getAbsolutePath(), dexFile.getAbsolutePath(), null, classLoader.getParent()); try { //通过反射获取到PathClassLoader的parent成员变量 Field fieldClassLoader = ClassLoader.class.getDeclaredField("parent"); if (fieldClassLoader != null) { //把parent成员变量赋值为DexClassLoader fieldClassLoader.setAccessible(true); fieldClassLoader.set(classLoader, dexClassLoader); } } catch (Exception e) { e.printStackTrace(); } }
这样就实现了DexClassLoader的插入,每次加载app的类之前都会通过DexClassLoader指定的位置查找是否有要用来覆盖的类。
新的双亲委派流程图如下: