Java 中的 ClassLoader
能够加载 jar 文件和 Class文件(本质是加载 Class 文件),这一点在 Android 中并不适用,由于不管 DVM 仍是 ART 它们加载的再也不是 Class 文件,而是 dex 文件。java
Android 中的 ClassLoader
类型和 Java 中的 ClassLoader
类型相似,也分为两种类型,分别是系统 ClassLoader
和自定义 ClassLoader
。其中 Android 系统 ClassLoader
包括三种分别是 BootClassLoader
、PathClassLoader
和 DexClassLoader
,而 Java 系统类加载器也包括3种,分别是 Bootstrap ClassLoader
、 Extensions ClassLoader
和 App ClassLoader
。git
Android 系统启动时会使用 BootClassLoader
来预加载经常使用类,与 Java 中的 BootClassLoader
不一样,它并非由 C/C++ 代码实现,而是由 Java 实现的,BootClassLoade
的代码以下所示github
// 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
的构造方法有三个参数:markdown
DexClassLoader
能够加载 dex 文件以及包含 dex 的 apk 文件或 jar 文件,也支持从 SD 卡进行加载,这也就意味着 DexClassLoader
能够在应用未安装的状况下加载 dex 相关文件。所以,它是热修复和插件化技术的基础。app
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
中。ide
关于以上 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
能够参考 Github 上的这个项目