初探Android 热修复

本篇博客是笔者第一次接触热修复的学习笔记,主要分享内容:

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

为什么要用这样看着就很麻烦的机制来加载类呢?

  1. 防止类重复加载。同一个类只会由一个类加载器加载,避免了混乱。
  2. 隔离作用。保证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

小结

本篇内容主要是初探热修复的学习记录,可能会有很多不到之处,如若发现,请望指证。

参考了以下写的不错的博客:

一篇文章搞懂热修复类加载方案原理

从 CLASSLOADER 到热修复

热修复框架对比和代码修复

Android虚拟机框架:类加载机制

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值