Java 中的 ClassLoader
可以加载 jar 文件和 Class文件(本质是加载 Class 文件),这一点在 Android 中并不适用,因为无论 DVM 还是 ART 它们加载的不再是 Class 文件,而是 dex 文件。
Android 中的 ClassLoader
类型和 Java 中的 ClassLoader
类型类似,也分为两种类型,分别是 系统 ClassLoader
和 自定义 ClassLoader
。其中 Android 系统 ClassLoader
包括三种分别是 BootClassLoader
、 PathClassLoader
和 DexClassLoader
,而 Java 系统类加载器也包括3种,分别是 Bootstrap ClassLoader
、 Extensions ClassLoader
和 App ClassLoader
。
Android 系统启动时会使用 BootClassLoader
来预加载常用类,与 Java 中的 BootClassLoader
不同,它并是由 C/C++ 代码实现,而是由 Java 实现的,1BootClassLoade1 的代码如下所示
// libcore/ojluni/src/main/java/java/lang/ClassLoader.java class BootClassLoader extends ClassLoader { private static BootClassLoader instance; @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED") public static synchronized BootClassLoader getInstance() { if (instance == null) { instance = new BootClassLoader(); } return instance; } public BootClassLoader() { super(null); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { return Class.classForName(name, false, null); } ... } 复制代码
BootClassLoader
是 ClassLoader
的内部类,并继承自 ClassLoader
。 BootClassLoader
是一个单例类, 需要注意的是 BootClassLoader
的访问修饰符是默认的,只有在同一个包中才可以访问,因此我们在应用程序中是无法直接调用的 。
Android 系统使用 PathClassLoader
来加载系统类和应用程序的类,如果是加载非系统应用程序类,则会加载 data/app/$packagename
下的 dex 文件以及包含 dex 的 apk 文件或 jar 文件,不管是加载哪种文件,最终都是要加载 dex 文件,在这里为了方便理解,我们将 dex 文件以及包含 dex 的 apk 文件或 jar 文件统称为 dex 相关文件。 PathClassLoader 不建议开发直接使用。
// libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java 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); } } 复制代码
PathClassLoader
继承自 BaseDexClassLoader
,很明显 PathClassLoader
的方法实现都在 BaseDexClassLoader
中。
PathClassLoader
的构造方法有三个参数:
DexClassLoader
可以加载 dex 文件以及包含 dex 的 apk 文件或 jar 文件,也支持从 SD 卡进行加载,这也就意味着 DexClassLoader
可以在应用未安装的情况下加载 dex 相关文件。 因此,它是热修复和插件化技术的基础。
public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); } } 复制代码
DexClassLoader
构造方法的参数要比 PathClassLoader
多一个 optimizedDirectory
参数,参数 optimizedDirectory
代表什么呢?应用程序在第一次被加载的时候,为了提高以后的启动速度和执行效率,Android 系统会对 dex 相关文件做一定程度的优化,并生成一个 ODEX
文件,此后再运行这个应用程序的时候,只要加载优化过的 ODEX
文件就行了,省去了每次都要优化的时间,而参数 optimizedDirectory
就是代表存储 ODEX
文件的路径,这个路径必须是一个内部存储路径。 PathClassLoader
没有参数 optimizedDirectory
,这是因为 PathClassLoader
已经默认了参数 optimizedDirectory
的路径为: /data/dalvik-cache
。 DexClassLoader
也继承自 BaseDexClassLoader
,方法实现也都在 BaseDexClassLoader
中。
关于以上 ClassLoader
在 Android 系统中的创建过程,这里牵扯到 Zygote
进程,非本文的重点,故不在此进行讨论。
ClassLoader
是一个抽象类,其中定义了 ClassLoader
的主要功能。 BootClassLoader
是它的内部类。 SecureClassLoader
类和 JDK8
中的 SecureClassLoader
类的代码是一样的,它继承了抽象类 ClassLoader
。 SecureClassLoader
并不是 ClassLoader
的实现类,而是拓展了 ClassLoader
类加入了权限方面的功能,加强了 ClassLoader
的安全性。 URLClassLoader
类和 JDK8
中的 URLClassLoader
类的代码是一样的,它继承自 SecureClassLoader
,用来通过URl路径从 jar 文件和文件夹中加载类和资源。 BaseDexClassLoader
继承自 ClassLoader
,是抽象类 ClassLoader
的具体实现类, PathClassLoader
和 DexClassLoader
都继承它。 下面看看运行一个 Android 程序需要用到几种类型的类加载器
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) var classLoader = this.classLoader // 打印 ClassLoader 继承关系 while (classLoader != null) { Log.d("MainActivity", classLoader.toString()) classLoader = classLoader.parent } } } 复制代码
将 MainActivity
的类加载器打印出来,并且打印当前类加载器的父加载器,直到没有父加载器,则终止循环。打印结果如下:
com.zhgqthomas.github.hotfixdemo D/MainActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.zhgqthomas.github.hotfixdemo-2/base.apk"],nativeLibraryDirectories=[/data/app/com.zhgqthomas.github.hotfixdemo-2/lib/arm64, /oem/lib64, /system/lib64, /vendor/lib64]]] com.zhgqthomas.github.hotfixdemo D/MainActivity: java.lang.BootClassLoader@4d7e926 复制代码
可以看到有两种类加载器,一种是 PathClassLoader
,另一种则是 BootClassLoader
。 DexPathList
中包含了很多路径,其中 /data/app/com.zhgqthomas.github.hotfixdemo-2/base.apk
就是示例应用安装在手机上的位置。
类加载器查找 Class 所采用的是双亲委托模式,**所谓双亲委托模式就是首先判断该 Class 是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的 BootstrapClassLoader
,如果 BootstrapClassLoader
找到了该 Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找。这是 JDK 中 ClassLoader
的实现逻辑,Android 中的 ClassLoader
在 findBootstrapClassOrNull
方法的逻辑处理上存在差异。
// ClassLoader.java protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 委托父加载器进行查找 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) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats } } return c; } 复制代码
上面的代码很容易理解,首先会查找加载类是否已经被加载了,如果是直接返回,否则委托给父加载器进行查找,直到没有父加载器则会调用 findBootstrapClassOrNull
方法。
下面看一下 findBootstrapClassOrNull
在 JDK
和 Android
中分别是如果实现的
// JDK ClassLoader.java private Class<?> findBootstrapClassOrNull(String name) { if (!checkName(name)) return null; return findBootstrapClass(name); } 复制代码
JDK
中 findBootstrapClassOrNull
会最终交由 BootstrapClassLoader
去查找 Class
文件,上面提到过 BootstrapClassLoader
是由 C++ 实现的,所以 findBootstrapClass
是一个 native 的方法
// JDK ClassLoader.java private native Class<?> findBootstrapClass(String name); 复制代码
在 Android 中 findBootstrapClassOrNull
的实现跟 JDK
是有差别的
// Android private Class<?> findBootstrapClassOrNull(String name) { return null; } 复制代码
Android
中因为不需要使用到 BootstrapClassLoader
所以该方法直接返回来 null
正是利用类加载器查找 Class 采用的双亲委托模式,所以可以利用反射修改类加载器加载 dex 相关文件的顺序,从而达到热修复的目的
通过上面分析可知
PathClassLoader
可以加载 Android 系统中的 dex 文件 DexClassLoader
可以加载任意目录的 dex/zip/apk/jar
文件,但是要指定 optimizedDirectory
。 通过代码可知这两个类只是继承了 BaseDexClassLoader
,具体的实现依旧是由 BaseDexClassLoader
来完成。
// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java public class BaseDexClassLoader extends ClassLoader { ... private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { this(dexPath, optimizedDirectory, librarySearchPath, parent, false); } /** * @hide */ public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent, boolean isTrusted) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted); if (reporter != null) { reportClassLoaderChain(); } } ... public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) { // TODO We should support giving this a library search path maybe. super(parent); this.pathList = new DexPathList(this, dexFiles); } ... } 复制代码
通过 BaseDexClassLoader
构造方法可以知道,最重要的是去初始化 pathList
也就是 DexPathList
这个类,该类主要是用于管理 dex 相关文件
// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java @Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); // 查找逻辑交给 DexPathList if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException( "Didn't find class /"" + name + "/" on path: " + pathList); for (Throwable t : suppressedExceptions) { cnfe.addSuppressed(t); } throw cnfe; } return c; } 复制代码
BaseDexClassLoader
中最重要的是这个 findClass
方法,这个方法用来加载 dex 文件中对应的 class
文件。而最终是交由 DexPathList
类来处理实现 findClass
// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java final class DexPathList { ... /** class definition context */ private final ClassLoader definingContext; /** * List of dex/resource (class path) elements. * Should be called pathElements, but the Facebook app uses reflection * to modify 'dexElements' (http://b/7726934). */ private Element[] dexElements; ... DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) { ... this.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); // save dexPath for BaseDexClassLoader this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted); ... } } 复制代码
查看 DexPathList
核心构造函数的代码可知, DexPathList
类通过 Element
来存储 dex 路径
,并且通过 makeDexElements
函数来加载 dex 相关文件,并返回 Element
集合
// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java private static Element[] makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) { Element[] elements = new Element[files.size()]; int elementsPos = 0; /* * Open all files and load the (direct or contained) dex files up front. */ for (File file : files) { if (file.isDirectory()) { // We support directories for looking up resources. Looking up resources in // directories is useful for running libcore tests. elements[elementsPos++] = new Element(file); } else if (file.isFile()) { String name = file.getName(); DexFile dex = null; if (name.endsWith(DEX_SUFFIX)) { // 判断是否是 dex 文件 // Raw dex file (not inside a zip/jar). try { 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 { // 如果是 apk, jar, zip 等文件 try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { /* * IOException might get thrown "legitimately" by the DexFile constructor if * the zip file turns out to be resource-only (that is, no classes.dex file * in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */ suppressedExceptions.add(suppressed); } // 将 dex 文件或压缩文件包装成 Element 对象,并添加到 Element 集合中 if (dex == null) { elements[elementsPos++] = new Element(file); } else { elements[elementsPos++] = new Element(dex, file); } } if (dex != null && isTrusted) { dex.setTrusted(); } } else { System.logW("ClassLoader referenced unknown path: " + file); } } if (elementsPos != elements.length) { elements = Arrays.copyOf(elements, elementsPos); } return elements; } 复制代码
总体来说, DexPathList
的构造函数是将 dex 相关文件(可能是 dex、apk、jar、zip , 这些类型在一开始时就定义好了)封装成一个 Element
对象,最后添加到 Element
集合中
其实,Android 的类加载器不管是 PathClassLoader,还是 DexClassLoader,它们最后只认 dex 文件,而 loadDexFile
是加载 dex 文件的核心方法,可以从 jar、apk、zip 中提取出 dex
// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java public Class<?> findClass(String name, List<Throwable> suppressed) { 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; } 复制代码
在 DexPathList
的构造函数中已经初始化了 dexElements
,所以这个方法就很好理解了,只是对 Element 数组进行遍历,一旦找到类名与 name 相同的类时,就直接返回这个 class,找不到则返回 null
通过上面的分析可以知道运行一个 Android
程序是使用到 PathClassLoader
,即 BaseDexClassLoader
,而 apk 中的 dex 相关文件都会存储在 BaseDexClassLoader
的 pathList
对象的 dexElements
属性中。
那么热修复的原理就是将改好 bug 的 dex 相关文件放进 dexElements
集合的头部,这样遍历时会首先遍历修复好的 dex 并找到修复好的类,因为类加载器的双亲委托模式,旧 dex 中的存有 bug 的 class 是没有机会上场的。这样就能实现在没有发布新版本的情况下,修复现有的 bug class
根据上面热修复的原理,对应的思路可归纳如下
BaseDexClassLoader
的子类 DexClassLoader
加载器 dexElements
进行合并,并设置自由的 dexElements
优先级 pathList