热修复、mutidex等都是基于安卓动态加载实现的动态插装dex文件的应用实例,那么究竟他们都是如何实现的,让我们花些时间了解一下原理。
ClassLoader
ClassLoader 和 BootClassLoader
看安卓中ClassLoader的源码实现可以看到,它是一个抽象类,构造器中需要传入一个parent ClassLoader并且不能为空,默认的parent ClassLoader为PathClassLoader且它的parent的ClassLoader为BootClassLoader。
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
return new PathClassLoader(classPath, BootClassLoader.getInstance());
}
BootClassLoader是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用和修改。
双亲代理模型加载
刚才提到的parent ClassLoader如何理解?这里的ClassLoader是抽象类,所有的ClassLoader实现类都一定继承自这个抽象类,并且通过代理模式的方式传入了一个parent ClassLoader实例,就算不传,也会默认给你生成一个PathClassLoader作为parent ClassLoader,可以理解为多继承,这就是双亲代理模型。
在ClassLoader中最核心的方法是loadClass方法,在java1.2之后不建议子类重写该方法,而是建议子类修改findClass方法。
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
可以看到loadClass方法在加载一个类的实例的时候,会先查询当前ClassLoader实例是否加载过此类,有就返回,如果没有就查询parent ClassLoader是否已经加载过此类,如果已经加载过,就直接返回parent加载的类,如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作。也就是说如果一个类被位于树根的ClassLoader加载过,那么在以后整个系统的生命周期内,这个类永远不会被重新加载。
通过这种方式可以更好的实现共享和隔离,同一个类不需要被重复的加载,同时一些核心类也不会被用户的同名类替换,更加安全。
在这里也可以抛出一个问题,当我们使用热修复修复代码时,两个同样的类只要保证新的类被优先加载,旧的类就不会生效,这便是像nuwa这种热修复框架的原理,那么等下我们再看如何保证新类被优先加载这个问题。
DexClassLoader 和 PathClassLoader
在Android中,我们一般是使用DexClassLoader、PathClassLoader这些类加载器来加载类,它们的不同之处是:
- DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;
- PathClassLoader只能加载系统中已经安装过的apk;
// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
// PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
从代码中可以看到DexClassLoader和PathClassLoader主要不同是在于传入的optimizedDirectory一个为null,一个不为null。
optimizedDirectory是一个内部存储路径,DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,我们可以把外部的dex复制到optimizedDirectory路径下;而PathClassLoader没有optimizedDirectory,会有个默认的路径,只能加载系统的类和已经安装的应用apk的类。这个ClassLoader不建议开发者使用。
// BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
……
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = new ZipFile(file);
}
……
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
/**
* Converts a dex/jar file path and an output directory to an
* output file path for an associated optimized dex file.
*/
private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
在主要的实现类BaseDexClassLoader中,有一个DexPathList对象pathList,在这个对象中有个Element数组对象dexElements,通过调用一个private方法makeDexElements生成的一个dex列表。
Load Class
在app启动的时候会创建一个PathClassLoader,用来加载apk文件中第一个dex,也就是说严格意义上讲,我们一个app的所有class都是通过这个PathClassLoader加载进入davlik虚拟机的,也就是我们Context内的getClassLoader返回的对象。
class文件是通过ClassLoader的loadClass方法被load进虚拟机, 在第一次时会通过findClass方法来加载class,实际上调用的是BaseClassLoader的findClass方法:
// BaseDexClassLoader
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
// DexPathList
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
可以看到最终是通过循环遍历pathList中的dexElements列表,通过每个dexFile二分查找class文件,命中即返回。刚才提到过dexElements,里面存放的是个dex列表,这个列表是在classLoader生成过程中就被写入到内存的。
这样整个loadClass的过程就走通了,接下来我们看看mutidex和nuwa都干了些什么,顺便回答我们上面提出的一个问题——如何保证新类被优先加载。
MutiDex
MutiDex的原理我就不展开了,感觉这两篇还不错,一篇讲源码一篇讲使用
http://www.jianshu.com/p/79a14d340cb0
http://souly.cn/%E6%8A%80%E6%9C%AF%E5%8D%9A%E6%96%87/2016/02/25/android%E5%88%86%E5%8C%85%E5%8E%9F%E7%90%86/
看mutidex核心插入dex的代码:
private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
// 通过反射获取 ClassLoader中的pathList
Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
// 先调用pathList的makeDexElements,然后将生成的Element[]传入expandFieldArray中
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));
}
private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class});
return (Object[])((Object[])makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory}));
}
MultiDex.expandFieldArray方法的实现如下:
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[])((Object[])jlrField.get(instance));
Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
jlrField.set(instance, combined);
}
将dex2...3...4插到了dexElements主dex后面,从而实现了动态插装。
这个事情发生在attachBaseContext的回调,看源码可以知道,它发生的时间比Application onCreate的回调靠前,所以就意味着在onCreate的时候所有的dex代码都已经插装完成了。
一般我们使用nuwa插入代码的调用是发生在onCreate的回调中,看nuwa的核心源码:
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
Object newDexElements = getDexElements(getPathList(dexClassLoader));
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
private static Object combineArray(Object firstArray, Object secondArray) {
Class<?> loadClass = firstArray.getClass().getComponentType();
int firstArrayLength = Array.getLength(firstArray);
int allLength = firstArrayLength + Array.getLength(secondArray);
Object result = Array.newInstance(loadClass, allLength);
for (int k = 0; k < allLength; ++k) {
if (k < firstArrayLength) {
Array.set(result, k, Array.get(firstArray, k));
} else {
Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
}
}
return result;
}
注意nuwa是把dex文件插在了列表的最前面,这就回答了我一开始的问题,只要命中了新dex中的class,就不会再向老的dex中查找class了,从而实现了类的替换,也就实现了热修复。
那么再说nuwa的生效时间,在我理解只要nuwa加载的时候,需要修复的class文件还没有被davlik虚拟机加载过,就可以实现修复的效果,否则就无法修复,不知道这样理解是否正确。
再说到最近最火的热修复Tinker,下次把Tinker原理研究一波。