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 实现的, BootClassLoade 的代码如下所示
// 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 进程,非本文的重点,故不在此进行讨论。
下面看看运行一个 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 相关文件的顺序,从而达到热修复的目的
通过上面分析可知
通过代码可知这两个类只是继承了 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
根据上面热修复的原理,对应的思路可归纳如下