说明:该篇博客参考https://juejin.im/post/5a0ad2b551882531ba1077a2,只是为了自己的学习做记录,如有侵权请联系删除。
为了能够对热修复的原理理解的更加深入有必要对Android中dex文件的加载机制进行解析。
在Andorid中有两个专门的类加载器用于加载Andorid的dex文件中的class文件,分别是PathClassLoader和DexClassLoader;PathClassLoader只能加载已经安装到Andorid系统中的apk文件(data/aap目录),是Android系统默认的类加载器,DexClassLoader可以加载任意目录下的dex、jar、zip、apk文件,比PathClassLoader更加灵活,因此这也成为了实现热修复的一个突破点。下面对他们的代码分别进行讲解。
该类继承了BaseDexClassLoader类,并且在仅有的两个构造方法中也调用到了父类的构造方法中。
public class PathClassLoader extends BaseDexClassLoader {
/**
* dexPath:要加载的dex、jar、apk或者zip文件string路径列表,并且每一个dex路径用:分隔开
* parent:父类加载
**/
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
/**
* librarySearchPath:加载程序文件时需要用到的库路径,有可能为null
**/
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
复制代码
该类也是继承了BaseDexClassLoader了,并且在仅有的一个构造方法中调用到了父类的构造方法。
public class DexClassLoader extends BaseDexClassLoader {
/**
* optimizedDirectory:dex文件的输出目录,因为在加载zip、apk、jar格式的程序文件的时候会解压出其中的dex文件,该目录
*就是专门用于存放这些被解压出来的dex文件,但是从api26开始就失效了,即使传入了具体的值也不会被使用。
**/
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
复制代码
DexClassLoader和PathClassLoader类加载器最终都会调用到BaseDexClassLoader类中,也就是具体的实现都在该类中。
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();
}
}
复制代码
从源码可以看出BaseDexClassLoader类继承了ClassLoader类,并且在BaseDexClassLoader的构造方法中首先会调用父类的构造方法,下面对ClassLoader中的构造方法进行分析。
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
复制代码
在ClassLoader构造方法中会对父加载器进行初始化。接下来继续看BaseDexClassLoader的构造方法,初始化了成员变量pathList,继续看DexPathList中的构造方法。
/**
* definingContext:当前的类加载器
* dexPath:要加载的dex、jar、apk或者zip文件string路径列表,每一个dex路径用:分隔开
* librarySearchPath:加载程序文件的库文件
* optimizedDirectory:dex文件的解压目录,但是在api26以后就不在使用了
**/
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
..........//判断数据的合法性
this.definingContext = definingContext;
//初始化IO异常列表
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//将dex文件构造为Elements对象
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
//对库文件路径进行解析
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
复制代码
/**
* searchPath:要加载的dex、jar、apk或者zip文件string路径列表,每一个dex路径用:分隔开
**/
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
List<File> result = new ArrayList<>();
if (searchPath != null) {
for (String path : searchPath.split(File.pathSeparator)) {
if (directoriesOnly) {
try {
StructStat sb = Libcore.os.stat(path);
if (!S_ISDIR(sb.st_mode)) {
continue;
}
} catch (ErrnoException ignored) {
continue;
}
}
//将string列表中单个dex、jar、apk或者zip文件路径存放到list中
result.add(new File(path));
}
}
return result;
}
复制代码
/**
* files:dex、jar、zip或者apk文件路径列表
* optimizedDirectory: dex解压路径,在api26以后为null
* suppressedExceptions:IO异常列表
**/
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;
for (File file : files) {
//文件是一个目录,则直接添加到elements列表中,后续解析的时候直接从目录中找到dex文件
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
//如果该文件是.dex结尾的文件则将该文件包装为DexFile对象
if (name.endsWith(DEX_SUFFIX)) {
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 {
//如果该文件是jar、apk或者zip文件,则从这些文件中提取出dex文件并包装太DexFile对象,具体的提取是在DexFile
//中通过native方法进行提取
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
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);
}
}
//如果实际的长度和理论的长度不等,则将elements的长度变更为实际长度
//实际长度<=理论长度
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
复制代码
从PathClassLoader和DexClassLoader的构造方法开始,最后会在BaseClassLoader中将包含dex的文件或者文件夹构造成一个个的Element对象,并且最后会通过findClass方法从构造出的Element列表中解析出与传入的class名相同的class文件。下面对findClass方法进行分析。
/**
* 遍历dexElements列表,找到与传入的className相对应的第一个class并返回;正因为这个特性成为了热修复的突破点,我们只需要
* 将需要修复的bug类编译成dex文件然后放到dexElements列表的第一个元素位置,当系统在查找类的时候就会只加载我们插入的dex
* 文件
* name: 需要寻找的class名
* suppressed: 异常列表
**/
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;
}
复制代码
在BaseClassLoader的findClass方法中最终会调用到Element的findClass方法。
//最终会从dex文件查找该class,如果找到了则直接返回,没有找到则直接返回null
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
复制代码
最终会调用到DexFile中的loadClassBinaryName方法
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
//调用到native层从dex文件中查找到与name相对应的clas文件
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
复制代码
文章最开始就讲到Android中存在两个类加载器PathClassLoader和DexClassLoader,它们虽然都是为了将一个个dex文件构造成Element对象,并从dex文件中加载出对应的class文件,但是它们的使用方式却不相同。PathClassLoader是Android默认的dex文件加载器,DexClassLoader则是为了能够加载没有被初始化在apk中的代码,它可以加载Android中任意目录下包含dex的jar、apk、zip等文件,而这也成为了我们实现热修复的突破点。根据这种思路实现热修复大致步骤如下:
(1)将需要加入到原有apk的java文件编译为dex文件格式;
(2)获取到默认的PathClassLoader实例对象;
(3)获取指定目录下面所有包含dex文件的apk、jar、zip等文件;
(4)根据获取到的文件构造出DexClassLoader;
(5)获取到DexClassLoader中的dexElements列表,并存储到集合中;
(6)获取PathClassLoader中的dexElements列表;
(7)将获取到的dexElements列表集合按先后顺序存储到PathClassLoader中dexElements列表中的头部;
当app重新启动之后就会加载最新的dex文件,这样就会将Bug修复了,不过老的dex文件依旧存在于dexElements列表中,只是没有机会被加载到了而已。具体可以参考文章开头中这位大神的实现。