Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。
Android平台的ClassLoader
我们主要用到了两种类加载器 PathClassLoader 和 DexClassLoader 。
他们的区别是:
PathClassLoader:是 Android 应用中默认的类加载器,只能加载已经安装到 Android 系统中的apk文件(/data/app目录下,解压为 dex 后优化为 odex)。
DexClassLoader:可以加载路径下的 dex/jar/apk/zip 文件,比 PathClassLoader 更灵活,是实现热修复的关键。
首先说下双亲委托模式
双亲委托模式的特点
类加载器查找Class所采用的是双亲委托模式,所谓双亲委托模式就是首先判断该Class是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了该Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找。
这样讲可能会有些抽象,来看下面的图。
再说dex文件的加载和类的查找过程
Java层通过我们会通过创建一个DexClassLoader来加载我们的dex,下面就以此为切入点进行
创建ClassLoader
dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());
查看DexClassLoader的构造方法。
public class DexClassLoader extends BaseDexClassLoader {
// dexPath:是加载apk/dex/jar的路径
// optimizedDirectory:是优化dex后得到的.odex文件的输出路径
// libraryPath:是加载的时候需要用到的so库
// parent:给DexClassLoader指定父加载器
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
可以看到它调用的是父类的构造函数,所以直接来看BaseDexClassLoader的构造函数。
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
创建了一个DexPathList实例,下面来看看DexPathList的构造函数。
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
}
//它调用的是makeDexElements方法来创建一个Element数组来存放Element对象,
//每个Element对象包含一个DexFile对象。
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
// 如果是一个dex文件
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
// 如果是一个apk或者jar或者zip文件
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
try {
// 1、调用loadDexFile加载dex文件,得到一个DexFile对象
// loadDexFile通过c++层native方法去加载dex文件
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
// 2、把DexFile对象封装到Element对象中,然后将Element对象加入Element数组
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
dex文件的加载流程:我们会使用DexClassLoader去加载dex文件,DexClassLoader会将这个任务委派给DexPathList中的makeDexElements方法,在makeDexElements中调用了native层的 c++方法去真正的加载dex文件,然后返回DexFile的对象,通过这个对象构建一个Element的对象,然后将这个Element添加到dexElements的数组中。
类的加载过程
当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程,源码如下
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 首先从已经加载的类中查找
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
// 如果没有加载过,先调用父加载器的 loadClass
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;
}
一句话概括:ClassLoader 加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的 findClass 方法加载,该逻辑避免了类的重复加载。所以我们所要实现的就是把要替换的类可见性提前,这样类加载器就会优先找到修复过的类。
类的查找过程
//DexClassLoader间接调用父类findClass方法
//,findClass方法中调用DexPathList中的DexPathList方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
//看DexPathList中的findClass方法,可以看到它是遍历dexElements数组,
//到每个dex文件去寻找当前需要的类,找到之后直接返回不往下找了
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;
}
类的查找过程:DexClassLoader通过findClass去查找一个类,同样它也是委派给DexPathList的findClass去查找,在DexPathList的findClass中会去遍历我们上面创建的dexElements数组,然后在每个dex中去查找相应的类,找到之后就返回,不再向后查找。
热修复过程
1、PathClassLoader 作为默认的类加载器,也就是第一个 DEX 文件是 PathClassLoader 自动加载的。
2、通过前面的分析来看,我们知道 PathClassLoader 里面的 DEX 文件是存放在一个 Element 数组中,可以包含多个 DEX 文件,所以我们只需要通过反射获取 PathClassLoader 中的 DexPathList 中的Element数组(已加载了第一个dex包,由系统加载),将要替换的 DEX 文件放置到这个数组中去。
3、将两个Element数组合并之后,再将其赋值给 PathClassLoader 的 Element 数组
Bugly热更新是基于Tinker的 Bugly 热更新原理图如下:
如版本 1.0.0 上线后有bug 启动1.0.0 版本app的时候 会把当前对应的TinkerId上报到平台,
修改相关代码 执行第二步骤 打补丁包后 补丁包里面会有一个YAPATH.md文件,from 表示我发布的版本, to 表示我现在打补丁包,意思就是 这个1.0.0-patch这个补丁包 是针对于1.0.0-base 来修复的,
上传补丁包到配置平台后,平台会自动解析YAPATH.md文件, 然后针对性的下发补丁。