本篇博客是笔者第一次接触热修复的学习笔记,主要分享内容:
1. 什么是android的热修复?
2. 如何实现热修复?目前有什么方案?
3. 热修复背后的原理?
Android热修复是啥?
常见的使用场景
- 刚发布的应用出现闪退、ANR等bug,及时修复 。
- 及时推送一些小的功能给用户使用。
优势所在
- 无需重新发布,实时高效修复bug
- 用户无需操作,无需下载新的应用
- 修复成功率高,降低损失
热修复方案和背后的原理
热修复主要分为以下三种类型的修复:
- 资源修复
- 代码修复
- 动态链接库(so)的修复
由于内容较多,本篇博客只分享代码修复。对于代码修复,主要有三种方案:
- 类加载方案
- 底层替换方案
- Instant Run方案
1. 类加载方案
类加载方案基于Dex分包方案。这个分包方案是为了解决65536方法数限制和LinearAlloc缓存区限制。
Dex分包方案主要做的是:在应用打包的时候,把代码分到多个Dex中,把应用启动的需要的类和相关引用的类放到主Dex中,其余放到次Dex中,应用启动的时候先加载主Dex,等应用启动完成再加载次Dex。这样就可以解决方法数的限制。
65536方法数限制和LinearAlloc缓存区限制?
65536限制是因为DVM指令集的方法调用指令invoke-kind的索引为16bits,所以最多引用65536个方法。
DVM的LinearAlloc是一个固定的缓存区,当方法数超出了缓存区的大小就会报错。
类加载方案的原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hLBjuPUY-1578657394612)(http://note.youdao.com/yws/res/31187/WEBRESOURCEa49b7933693950911b1477b9b56a6a63)]
Jvm主要是通过读取class字节码来加载, 而art虚拟机则是从dex字节码来读取. 这是一种更为优化的方案, 可以将多个.class文件合并成一个classes.dex文件。
Element内部封装了DexFile,用于加载dex文件,每个dex文件对应一个Element。多个Element组成了有序的Element数组dexElements。去加载类的时候,会遍历dex文件数组查找类,如果在Element中找到就直接返回,如果没有就在下一个Element中进行查找。
根据这么一套流程,就可以将修复完bug的class打成dex包,把它放在Element数组的第一个元素,这样去加载类的时候就会先遍历到修复bug的那个类,查找到后就可以进行加载。
到这里不知道你有没有疑惑:后面的存在bug的相同类,它仍可能会遍历到,那为什么它就不会被加载了呢?
这个因为类加载器有一个双亲委托模型。
双亲委托模型(Parent Delegation Model)是什么?
双亲委托模型是类加载器加载类时遵循的一套规则,Java的类加载器和Android类加载器都有这一套机制。
我们先看一下Java的类加载器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MJaI4lHu-1578657394615)(http://note.youdao.com/yws/res/31444/WEBRESOURCEda171332572c8a098bc9e3c246b781d6)]
所谓双亲委托模式就是首先判断该 Class 是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的BootstrapClassLoader。
如果 BootstrapClassLoader找到了该Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找。
have code , not bb
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 {
//去调用native方法findBootstrapClass
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;
}
Android 中的 ClassLoader 在 findBootstrapClassOrNull 方法的逻辑处理上和java 稍有区别。
JDK 中 findBootstrapClassOrNull 会最终交由 BootstrapClassLoader 去查找 Class 文件,BootstrapClassLoader 是由 C++ 实现的,是一个 native 的方法。
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
在 Android 中 findBootstrapClassOrNull 的实现
private Class<?> findBootstrapClassOrNull(String name)
{
return null;
}
因为Android不需要使用到 BootstrapClassLoader,所以该方法直接返回 null
为什么要用这样看着就很麻烦的机制来加载类呢?
- 防止类重复加载。同一个类只会由一个类加载器加载,避免了混乱。
- 隔离作用。保证java/Android核心类库的纯净和安全,防止恶意加载。 比如string类,避免用户自己写代码冒充核心类库。
那么Android的类加载器是怎么样的呢?看一下它的类图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fVc9W918-1578657394616)(http://note.youdao.com/yws/res/31558/WEBRESOURCEe4671b829f2468127d3d36223ef9e7c0)]
- BootClassLoader Android系统启动时会使用BootClassLoader来预加载常用类
- PathClassLoader 加载系统类和应用程序的类
- DexClassLoader DexClassLoader可以加载dex文件以及包含dex的压缩文件(apk和jar文件)
BaseDexClassLoader类里有个 pathList ,就是用它存储dex文件路径,它是 DexPathList 这个类的对象,内部有个 Element数组 来存储 dex 路径。修复后的dex包最终会通过反射技术赋值给pathList
类加载方案有个弊端:
它不能立即生效,需要重启App后让ClassLoader重新加载新的类。
那为什么需要重启呢?
这是因为类是无法被卸载的。
使用这套方案的主要是腾讯系,比如微信的Tinker、QQ空间的超级补丁、手机QQ的QFix。还有饿了么的Amigo和Nuwa等。
-
微信Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素。
-
饿了么的Amigo则是将补丁包中每个dex 对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element 数组。
2. 底层替换方案
与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类。底层替换方案有个明显的好处就是无需重启应用,可以立即生效应用。
但它存在一个很大的缺点:由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法。
在ART虚拟机中对应的一个ArtMethod指针,ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等等。
class ArtMethod FINAL {
...
protected:
GcRoot<mirror::Class> declaring_class_;
std::atomic<std::uint32_t> access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;
uint16_t hotness_count_;
struct PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;//1
void* data_;
void* entry_point_from_quick_compiled_code_;//2
} ptr_sized_fields_;
}
阿里的Andfix会将一个旧Java方法对应的ArtMethod实例中的所有字段值替换为新方法的值,这样所有执行到旧方法的地方,都会取得新方法的执行入口,所属class,方法索引,所属dex。像调用旧方法一样执行了新方法的逻辑。
但是这样会有兼容问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败。
阿里的Sophix为了解决了这个问题,它直接替换整个ArtMethod结构体,这样不会存在兼容问题。
采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。
3. Instant Run方案
AndroidStudio从2.0开始,加入了一个功能叫做InstantRun。 可以说Instant Run的出现推动了热修复框架的发展,市面上大多数资源热修复方案基本参考了Instant Run的实现。
Instant run的原理是采用了狸猫换太子的戏法,在编译阶段给每个类都注入了一个 c h a n g e ( 代 理 , 即 补 丁 ) 变 量 , 并 且 在 每 个 方 法 前 都 注 入 了 一 段 代 码 , 判 断 change(代理,即补丁)变量,并且在每个方法前都注入了一段代码,判断 change(代理,即补丁)变量,并且在每个方法前都注入了一段代码,判断change是否为空,如果不为空,就执行代理里的方法。
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
paramBundle });
return;
}
借鉴Instant Run的原理的热修复框架有美团的Robust、Aceso
小结
本篇内容主要是初探热修复的学习记录,可能会有很多不到之处,如若发现,请望指证。
参考了以下写的不错的博客: