动态修复bug:
- 下发补丁(内含修复好的class)到用户手机,即让app从服务器上下载(网络传输)
- app通过**"某种方式"**,使补丁中的class被app调用(本地更新)
一,双亲委派机制 :
当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程
工作过程
一句话概括:ClassLoader 加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的 findClass 方法加载,该逻辑避免了类的重复加载。所以我们所要实现的就是把要替换的类可见性提前,这样类加载器就会优先找到修复过的类。
ClassLoader中:
protected Class<?> loadClass(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;
}
1)特点:
如果一个类被classLoader继承线路上的任意一个加载过,那么在以后整个系统的生命周期中,这个类都不会再被加载,大大提高了类的加载效率。
2)作用:
1,避免类的重复加载,JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就属于两个不同的类(比如,Java中的Object类,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,如果不采用双亲委派模型,由各个类加载器自己去加载的话,系统中会存在多种不同的Object类)
2,保护程序安全,防止核心API被随意篡改,避免用户自己编写的类动态替换 Java的一些核心类,比如我们自定义类:java.lang.String
3,在JVM中表示两个class对象是否为同一个类存在两个必要条件:
类的完成类名必须一致,包括包名
加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
二,Android中的ClassLoader
Android中最主要的类加载器有如下4个:
- BootClassLoader:加载Android Framework层中的class字节码文件(类似java的Bootstrap ClassLoader)
- PathClassLoader:加载已经安装到系统中的Apk的class字节码文件(类似java的App ClassLoader)
- DexClassLoader:加载制定目录的class字节码文件(类似java中的Custom ClassLoader)
- BaseDexClassLoader:PathClassLoader和DexClassLoader的父类
1)使用场景
先来介绍一下这两种Classloader在使用场景上的区别
- PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
- DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。
那我们如何将新的 DEX 文件插入到上述的类加载器中呢?观察它们的父类BootClassLoader,可以发现有个成员变量 DexPathList,它的属性中有一个 dex 数组,也就是说我们只要将替换的类添加到 dexElements 前面即可,这样类加载器跟据双亲委托模型(Parent Delegation Model)的机制就会使用先找到的类。
三,热修复的实现原理
经过对PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,我们知道,安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组中获取(Element[] dexElements)到对应的类,之后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。在for循环中,首先遍历出来的是dex文件,然后再是从dex文件中获取class,所以,我们只要让修复好的class打包成一个dex文件,放于Element数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(当然,有bug的class也是存在的,不过是放在了Element数组的最后一个元素中,所以没有机会被拿到而已。
热修复类加载方案 :
类加载方案为什么需要重启呢?
这是因为类是无法被卸载的,因此要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的
- QQ空间的超级补丁和Nuwa是按照上面说得将补丁包放在Element数组的第一个元素得到优先加载
pre-verified错误: 假如原先有个DEX文件中类B引用了类A,旧的类A与类B在同一个DEX文件,则B会被打上CLASS_ISPREVERIFIED,现在修复DEX文件包含了类A,当类B某个方法引用到类A时尝试去解析类A。就会报错
- Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素
因为补丁包只是差异包,需要和本地的 dex 、资源等进行合成后,得到全量的 dex 才能被完整地使用。这样也就避免了热修复中 dex 的 pre-verify 问题,也减少了补丁包的体积
Tinker资源替换: 合并原始apk,得到fix.apk
1.反射拿到ActivityThread对象持有的LoadedApk容器
2.遍历容器中LoadedApk对象,反射替换mResDir属性为补丁物理路径.
3.创建新的AssetManager, 并根据补丁路径反射调用addAssetPath将补丁加载到新的AssetManager中.
4.反射获得ResourcesManager持有的Resources容器对象.
5.遍历出容器中的Resources对象, 替换对象的属性为新的AssetManager, 并且根据原属性重新更新Resources对象的配置.
- 饿了么的Amigo则是将补丁包中每个dex 对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element 数组。
热更demo流程图: github.com/luxiao0314/…
1,从sdk卡获取.zip,.dex,.jar,.apk文件,添加到set集合里面
四,Instant Run方案 :
Instant Run在第一次构建apk时,使用ASM在每一个方法中注入了类似如下的代码:
IncrementalChange localIncrementalChange = $change;//1
if (localIncrementalChange != null) {//2
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
paramBundle });
return;
}
其中注释1处是一个成员变量localIncrementalChange ,它的值为change实现了IncrementalChange这个抽象接口。当我们点击InstantRun时,如果方法没有变化则override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的override,因此满足了注释2的条件,会执行MainActivitydispatch方法,accessoverride的onCreate方法,从而实现了onCreate方法的修改。 借鉴Instant Run的原理的热修复框架有Robust和Aceso。
美团 Robust 热修复方案原理
下面,进入今天的主题,Robust热修复方案。首先,介绍一下 Robust 的实现原理。
以 State 类为例
public long getIndex() {
return 100L;
}
插桩后的 State 类
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}
//通过代理的形式,会调用StatePatch.accessDispatch实现返回值的替换
我们生成一个 StatePatch 类, 创一个实例并反射赋值给 State 的 changeQuickRedirect 变量。
public class StatePatch implements ChangeQuickRedirect {
@Override
public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
// 混淆后的 getIndex 方法 对应 a
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return 106;
}
return null;
}
@Override
public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return true;
}
return false;
}
}