说明:该篇博客参考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列表中,只是没有机会被加载到了而已。具体可以参考文章开头中这位大神的实现。