android 热修复方案总结

代码修复主要有三个方案,分别是底层替换方案、类加载方案和Instant Run方案。

类加载方案

在android类加载过程中,其中一个环节就是调用DexPathList的findClass方法,如下图所示。

 public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {//1
            Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
            if (clazz != null) {
                return clazz;
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

Element内部封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element。多个Element组成了有序的Element数组的dexElements。当要查找类时,会在注释1处遍历Element数组dexElements,注释2处调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。如果在Element中找到了该类就返回,如果没有找到就接着在下一个Element中进行查找。

根据上面的查找流程,我们将有bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素。这样会首先找到Path.dex中的Key.class去替换之前存在bug的Key.class。排在数组后面的dex文件中的存在bug的Key.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案。
这里写图片描述

类加载方案需要重启App后让ClassLoader重新加载新的类,这是因为类是无法被卸载的,因此要像重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。

虽然很多热修复框架采用了类加载方案,但具体的实现细节和步骤还是有一些区别的,比如QQ空间的超级不定和Nuwa是按照上面说得将补丁包放在Element数组的第一个元素得到优先加载。微信的Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中的apk的classes.dex做合并,生成新的classes.dex,然后再运行时通过反射将classes.dex放在Element数组的第一个元素。
这里写图片描述

底层替换方案

与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类,由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,如果我们增加了方法数,那么方案索引数也会增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况。

这里写图片描述

由于java层的每一个方法在虚拟机实现里面都对应一个ArtMethod的结构体,只要把原方法的结构体替换为新的结构体的内容,在调用原来方法的时候,真正执行的指令是新方法的指令,就是可以实现热修复。

具体的,在ART虚拟机中对应一个ArtMethod指针,ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等等,ArtMethod结构中比较重要的字段是注释1处的dex_cache_resolved_methods_和注释2处的entry_point_from_quick_compiled_code_,它们是方法的执行入口,当我们调用某一个方法时(比如Key的show方法),就会取得show方法的执行入口,通过执行入口就可以跳过去执行show方法。 替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,这就是底层替换方案。

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_;
}

不可否认,不同的安卓版本对应的java底层对应的ArtMethod是不一样的,因此不得不搞好多不同的case,处理不同的情况,但是每种case里不外乎是通过env->fromReflectedMethod得到由Method对象得到他对应的ArtMethod的真正的起始地址,把他强转为ArtMethod指针,就可以实现对所有的成员进行修改,也就是旧的干掉,新的进来。

Instant Run 方案

除了资源修复,代码修复同样也可以借鉴Instant Run 原理,可以说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,change 实现了IncrementalChange这个抽象接口,当我们点击InstantRun时,如果方法没有变化则change为null,调用return,不做任何处理。如果方法有变化,就生成替换类,这里我们假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivity override ,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的change设置为MainActivity override,因此满足了注释2的条件,会执行MainActivity override的access dispatch方法,access dispatch方法中会根据参数“onCreate.(Landroid/os/Bundle;)V”执行MainActivity$override的onCreate方法,从而实现了onCreate方法的修改。借鉴Instant Run的原理的热修复框架有Robust和Aceso.

参考文章:
https://blog.csdn.net/itachi85/article/details/79522200
https://blog.csdn.net/xiangzhihong8/article/details/77718004

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值