什么是热修复呢
热修复 就是我们的app在线上运行的时候出现了bug 可以在不发新版本安装包只用发布补丁包 用户无感知的前提下修复bug的技术
那么热修复要怎么实现呢
需要分为开发端 服务端 以及用户端三个部分
开发端的任务自然是修复bug 生成补丁包 服务端的任务主要是管理补丁包 而用户端自然就是下载执行补丁包
介绍一下几个主要的开源框架
AndFix与Robust的原理 即在编译打包阶段自动插入一段代码(字节码插桩) 类似于代理 将方法的执行代码重定向到其他方法中
那么Tinker的原理是什么呢
Tinker通过计算原始包中的dex与修复后的包中dex的区别生成补丁包dex 补丁包中的内容即为两者间差分的描述 运行时将补丁包与原始包中的dex文件进行合成 重启后生成新的修复后的dex文件
其实不管怎么怎样 热修复最终还是要基于类加载机制 以及反射去实现
所以必须要了解的 就是ClassLoader以及双亲加载机制
众所周知 我们编写的类在生成apk的时候 会打包成多个dex文件 而我们的程序在主线程起来之后 会创建ClassLoader(ActivityThread handleBindApplication)来加载dex
那么什么是ClassLoader呢
ClassLoader是一个抽象类 而他在我们系统里的实现 主要有BootClassLoader以及BaseDexClassLoader 其中BootClassLoader就是用于加载Android framework层的源码 那么BaseDexClassLoader就是用来加载我们编写类的dex 我们来看看BaseDexClassLoader的实现
public class BaseDexClassLoader extends ClassLoader {
// 需要加载的dex列表
private final DexPathList pathList;
// dexPath要加载的dex文件所在的路径,optimizedDirectory是odex将dexPath
// 处dex优化后输出到的路径,这个路径必须是手机内部路劲,libraryPath是需要
// 加载的C/C++库路径,parent是父类加载器对象
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List
// 使用pathList对象查找name类
Class c = pathList.findClass(name, suppressedExceptions);
return c;
}
}
可以看到BaseDexClassLoader 就是把dex文件的地址传给DexPathList 生成了一个DexPathList对象而findClass方法也是从DexPathList中查找需要注意的是BaseDexClassLoader构建的时候会把dexPath使用“:”分隔开传入多个dex文件路径 那么我们再来看一下DexPathList的实现
final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private final ClassLoader definingContext;
private final Element[] dexElements;
// 本地库目录
private final File[] nativeLibraryDirectories;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
// 当前类加载器的父类加载器
this.definingContext = definingContext;
ArrayList
// 根据输入的dexPath创建dex元素对象
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
}
可以看到 DexPathList 是通过makeDexElements 方法创建出了一个Element[]数组 我们不具体看这个方法的实现 总之就是把dex文件转换成了这个数组 也就相当于把我们实现的所有的类加载到了这个数组 至于为何是数组 因为我们的apk不止一个dex文件 那么我们再来看看findClass的实现
// 加载名字为name的class对象
public Class findClass(String name, List
// 遍历从dexPath查询到的dex和资源Element
for (Element element : dexElements) {
DexFile dex = element.dexFile;
// 如果当前的Element是dex文件元素
if (dex != null) {
// 使用DexFile.loadClassBinaryName加载类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
很清楚了对吧 就是通过遍历Element数组直到查找到我们要找的类
那么什么是双亲加载机制呢
我们先来看一下我们使用ClassLoader加载一个类是怎么实现的呢
protected ClassloadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c ==null) {
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.
c = findClass(name);
}
}
return c;
}
可以看到 查找的流程就是首先会去查找已经加载过的类 如果加载过就不会重新加载 如果没有加载过 就会去parent里面找 而如果parent没有找到 就会调用自己的findClass 前面我们说了 ClassLoader主要有两个实现类 BootClassLoader以及BaseDexClassLoader 这里的parent其实就是BootClassLoader 这也就是所谓双亲加载机制
为什么要有双亲加载机制呢
通过双亲加载机制 我们在加载一个类的时候会先从父类 也就是framework层的代码去找 这就保证了我们自己定义的类不会覆盖系统的类保证系统代码的安全
那么热修复究竟要怎么实现呢
这里我们讲替换dex文件思路的实现 前面我们讲到DexPathList 里面有一个Element数组我们在加载类的时候会遍历这个数组 那么我们的思路也就是通过反射Hook代码 把我们修复后的dex插入到这个数组的最前面 这样在加载类的时候就会首先加载我们修复好的类 也就修复了bug 当然实际运用可能还涉及到Android各个版本的兼容等问题并没有这么简单
但总体思路就是应用补丁包: patchElment(补丁包生成的) + oldElement(APK原有的) 赋值给oldElement
1、获取程序的PathClassLoader对象
2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
3、反射获取pathList的dexElements对象 (oldElement)
4、把补丁包变成Element数组:patchElement(反射执行makePathElements)
5、合并patchElement+oldElement = newElement (Array.newInstance)
6、反射把oldElement赋值成newElement
至于具体要怎么落地呢 我们下节再讲