热修复技术介绍
- 重新发布版本代价大,成本高,不及时,用户体验差,对此有几种解决方案:
- Hybird:原生+H5混合开发,缺点是人工成本搞,用户体验不如纯原生方案好;
- 插件化:移植成本高,对老代码的改造费时费力,而且无法动态修改;
- 热修复技术,将补丁上传到云端,app可以直接从云端下来补丁直接应用;
- 热修复技术对于国内开发者来说是一个比较实用的功能,可以解决如下问题:
- 发布新版本代价较大,用户下载安装成本高;
- 版本更新的效率问题,需要较长时间来完成版本覆盖;
- 版本更新的升级率问题,不升级版本的用户得不到修复,强更又比较暴力。
- 小而重要的功能,需要短时间内完成版本覆盖,比如节日活动。
- 热修复的优势:无需发版,用户无感知,修复成功率高,用时短;
百家争鸣的热修复框架
手淘的Dexposed: 开源,底层替换方案, 基于Xposed,针对Dalvik运行时的Java Method Hook技术,但对于Dalvik底层过于依赖,无法继续兼容Android5.0之后的ART,因此作罢;
支付宝的Andfix:开源,底层替换方案,借助Dexposed思想,做到了Dalvik和ART环境的全版本兼容,但其底层固定结构的替换方案稳定性不好,使用范围也存在着诸多限制,而且对于资源和so修复未能实现,详细原理参考:Android热修复框架AndFix原理解析及使用;
阿里百川的Hotfix:开源,底层替换方案,依赖于Andfix并对业务逻辑解耦,安全性和易用性较好,但还是存在Andfix的缺点;
Qzone超级补丁: 未开源,类加载方案,会侵入打包流程
美团的Robust:开源,Instant Run方案,详细可以参考美团技术团队的文章及Robust源码:Android热更新方案Robust, Android热更新方案Robust开源,新增自动化补丁工具
大众点评的Nuwa: 开源,类加载方案,具体实现可以参考:Android 热修复Nuwa的原理及Gradle插件源码解析
饿了么的Amigo:开源,类加载方案
微信的Tinker:开源,类加载方案,关于Tinker的原理可以看一下鸿洋的文章:Android 热修复 Tinker接入及源码浅析,
Android 热修复 Tinker 源码分析之DexDiff / DexPatch, Android 热修复 Tinker Gradle Plugin解析手淘的Sophix:未开源,商业收费,类加载方案+底层替换方案;(手淘团队基于Sophix有整理出一本电子书:深入探索Android热修复技术原理, 其中不仅讲了热修复原理还有许多编译相关的内容(关注微信公众号今阳说,回复关键字"热修复"领取书籍pdf))
热修复技术原理
- 热修复框架的核心技术主要有三类,分别是代码修复、资源修复和动态链接库修复
代码修复:
- 代码修复主要有三个方案,分别是底层替换方案、类加载方案和Instant Run方案
1. 类加载方案
- 类加载方案需要重启App后让ClassLoader重新加载新的类,因为类是无法被卸载的,要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。
优点:
- 不需要太多的适配;
- 实现简单,没有诸多限制;
缺点
- 需要APP重启才能生效(冷启动修复);
- dex插桩:Dalvik平台存在插桩导致的性能损耗,Art平台由于地址偏移问题导致补丁包可能过大的问题;
- dex替换:Dex合并内存消耗在vm head上,可能OOM,导致合并失败
- 虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,强制防止类被打上标志会影响性能;
Dex分包
- 类加载方案基于Dex分包方案,而Dex分包方案主要是为了解决65536限制和LinearAlloc限制:
- 65536限制:DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用 65535个方法;
- LinearAlloc限制:DVM中的LinearAlloc是一个固定的缓存区,当方法数过多超出了缓存区的大小,安装时提示INSTALL_FAILED_DEXOPT;
- Dex分包方案: 打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态的加载次Dex。主要有两种方案,分别是Google官方方案、Dex自动拆包和动态加载方案。
ClassLoader
- 在本系列的上一篇文章探索Android开源框架 - 10. 插件化原理中有讲过java中的ClassLoader(加载jar文件和Class文件,本质是加载Class文件), android中的ClassLoader(加载dex文件和apk文件), 双亲委派机制,以及ClassLoader如何加载插件中的类,其实热修复中代码修复的类加载方案也是使用的同样的原理;
几种不同的实现:
- 将补丁包放在Element数组的第一个元素得到优先加载(QQ空间的超级补丁和Nuwa)
- 将补丁包中每个dex 对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element 数组(饿了么的Amigo);
- 将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素(微信Tinker)
- Sophix:dex的比较粒度在类的维度,并且 重新编排了包中dex的顺序,classes.dex,classes2.dex..,可以看作是 dex文件级别的类插桩方案,对旧包中的dex顺序进行打破重组
2. 底层替换方案
- 其思想来源于Xposed框架,完美诠释了AOP编程,直接在Native层修改原有类(不需要重启APP),由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,因为这破坏原有类的结构(引起索引变化), 虽然限制多,但时效性好,加载轻快,立即见效;
优点
- 实时生效,不需要重新启动,加载轻快
缺点
- 兼容性差,由于 Android 系统每个版本的实现都有差别,所以需要做很多的兼容。
- 开发需要掌握 jni 相关知识, 而且native异常排查难度更高
- 由于无法新增方法和字段,无法做到功能发布级别
几种不同的实现:
- 采用替换ArtMethod结构体中的字段,这样会有兼容问题,因为手机厂商的修改 以及 android版本的迭代可能会导致底层ArtMethod结构的差异,导致方法替换失败;(AndFix)
- 同时使用类加载和底层替换方案,针对小修改,在底层替换方案限制范 围内,还会再判断所运行的机型是否支持底层替换方案,是就采用底层替换(替换整个ArtMethod结构体,这样不会存在兼容问题),否则使用类加载替换;(Sophix)
3. Instant Run方案
Instant Run新特性的原理就是当进行代码改动之后,会进行增量构建,也就是仅仅构建这部分改变的代码,并将这部分代码以补丁的形式增量地部署到设备上,然后进行代码的热替换,从而观察到代码替换所带来的效果。其实从某种意义上讲,Instant Run和热修复在本质上是一样的。
Instant Run打包逻辑
- 接入Instant Run之后,与传统方式相比,在进行打包的时候会存在以下四个不同点
- manifest注入:InstantRun会生成一个自己的application,然后将这个application注册到manifest配置文件里面,这样就可以在其中做一系列准备工作,然后再运行业务代码;
- nstant Run代码放入主dex:manifest注入之后,会将Instant Run的代码放入到Android虚拟机第一个加载的dex文件中,包括classes.dex和classes2.dex,这两个dex文件存放的都是Instant Run本身框架的代码,而没有任何业务层的代码。
- 工程代码插桩——IncretmentalChange;这个插装里面会涉及到具体的IncretmentalChange类。
- 工程代码放入instantrun.zip;这里的逻辑是当整个App运行起来之后才回去解压这个包里面的具体工程代码,运行整个业务逻辑。
- Instant Run在第一次构建apk时,使用ASM在每一个方法中注入了类似如下的代码 (ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能)
//$change实现了IncrementalChange这个抽象接口。
//当点击InstantRun时,如果方法没有变化则$change为null,就调用return,不做任何处理。
//如果方法有变化,就生成替换类,假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivity$override,
//这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法
//会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的$change设置为MainActivity$override
//因此满足了localIncrementalChange != null,会执行MainActivity$override的access$dispatch方法,
//access$dispatch方法中会根据参数”onCreate.(Landroid/os/Bundle;)V”执行MainActivity$override的onCreate方法,
//从而实现了onCreate方法的修改。
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {//2
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
paramBundle });
return;
}
被废弃的Instant Run
Android Studio 3.5 中一个显著变化是引入了 Apply Changes,它取代了旧的 Instant Run。Instant Run 是为了更容易地对应用程序进行小的更改并测试它们,但它会产生一些问题。为了解决这一问题,谷歌已经彻底删除了 Instant Run,并从根本上构建了 Apply Changes ,不再在构建过程中修改 APK,而是使用运行时工具动态地重新定义类,它应该比立刻运行更可靠和更快。
优点
- 实时生效,不需要重新启动
- 支持增加方法和类
- 支持方法级别的修复,包括静态方法
- 对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明
缺点
- 代码是侵入式的,会在原有的类中加入相关代码
- 会增大apk的体积
资源修复:
- 目前市面上大部分资源热修复方案基本都参考了Instant Run的实现, 其主要分两步:
- 创建新的AssetManager,并通过反射调用addAssetPath加载完整的新资源包;
- 找到所有之前引用到原有AssetManager的地方,通过反射,把引用处 替换为新AssetManager;
- 这里的具体原理可以参考章探索Android开源框架 - 10. 插件化原理中的资源加载部分;
- Sophix: 构造了一个package id为0x66的资源包(原有资源包为 0x7f),此包只包含改变了的资源项,然后直接在原有的AssetManager中 addAssetPath这个包就可以了,不修改AssetManager的引用处,替换更快更安全
so库修复:
- 主要是更新so,也就是重新加载so,主要用到了System的load和loadLibrary方法
- System.load(""): 传入so在磁盘的完整路径,用于加载指定路径的so
@CallerSensitive
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
- System.loadLibrary(""):传入so名称,用于加载app安装后自动从apk包中复制到/data/data/packagename/lib下的so
@CallerSensitive
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
- 最终都会调用到LoadNativeLibrary(),其主要做了如下工作:
- 判断so文件是否已经加载,若已经加载判断与class_Loader是否一样,避免so重复加载;
- 如果so文件没有被加载,打开so并得到so句柄,如果so句柄获取失败,就返回false,常见新的SharedLibrary,如果传入path对应的library为空指针,就将创建的SharedLibrary赋值给library,并将library存储到libraries_中;
- 查找JNI_OnLoad的函数指针,根据不同情况设置was_successful的值,最终返回该was_successful;
两种方案:
- 将so补丁插入到NativeLibraryElement数组的前部,让so补丁的路径先被返回和加载;
- 调用System.load方法来接管so的加载入口;
参考
- Android进阶解密
- Android热修复技术原理详解
- Android 插件化和热修复知识梳理
- 全面解析 Android 热修复原理
- 干货满满,Android热修复方案介绍
- 热修复原理学习(6)资源热修复技术
- Android热修复升级探索——SO库修复方案
- 深入探索Android热修复技术原理(关注微信公众号今阳说,回复关键字"热修复"领取书籍pdf)