1. 简述
热修复技术可谓是百花齐放,微信的Tinker、QQ空间的Nuwa、饿了么的Amigo、美团的Robust等等,各个热修复方案争相发布,都声称自己可以做到全方位全功能的热修复。不过他们各自有自身的局限性,或者不够稳定,或者补丁过大,或者效率低下,或者使用起来过于繁琐,大部分技术上看起来似乎可行,但实际体验并不好。
终于在2017年6月11日,手淘技术团队联合阿里云正式发布了新一代Android移动热修复方案——Sophix。Sophix的核心设计理念,就是非侵入性。Sophix这个名字,是来源于Sophic(明智的)+ FIX,一个更明智的热修复方案。
Sophix的横空出世,将会打破各家热修复技术纷争的局面。我们可以满怀信心地说,在Android热修复的三大领域:代码修复、资源修复、so修复方面,以及方案的安全性和易用性方面,Sophix都做到了业界领先。
2.优势
2.1 横向比较
方案对比 | Sophix | Tinker | Amigo |
---|---|---|---|
DEX修复 | 同时支持即时生效和冷启动修复 | 冷启动修复 | 冷启动修复 |
资源更新 | 差量包,不用合成 | 差量包,需要合成 | 全量包,不用合成 |
SO库更新 | 插桩实现,开发透明 | 替换接口,开发不透明 | 插桩实现,开发透明 |
性能损耗 | 低,仅冷启动情况下有些损耗 | 高,有合成操作 | 低,全量替换 |
四大组件 | 不能新增 | 不能新增 | 能新增 |
生成补丁 | 直接选择已经编好的新旧包在本地生成 | 编译新包时设置基线包 | 上传完整新包到服务端 |
补丁大小 | 小 | 小 | 大 |
接入成本 | 傻瓜式接入 | 复杂 | 一般 |
Android版本 | 全部支持 | 全部支持 | 全部支持 |
安全机制 | 加密传输及签名校验 | 加密传输及签名校验 | 加密传输及签名校验 |
服务端支持 | 支持服务端控制 | 支持服务端控制 | 支持服务端控制 |
可以看到,Sophix在各个指标上全面占优。而其中唯一支持不完善的地方就是四大组件,四大组件可以修改代码,但是无法做到新增。这是因为如果要新增四大组件,必须在AndroidManifest里面预先插入代理组件,并且尽可能声明所有权限,而这么做就会给原先的app添加很多臃肿的代码,对app运行流程的侵入性很强,所以,本着对开发者透明与代码极简的原则,它没有做这种多余的处理。
2.2 纵向比较
方案对比 | Andfix开源版本 | 阿里Hotfix 1.X | 阿里Hotfix最新版 (Sophix) |
---|---|---|---|
方法替换 | 支持,除部分情况[0] | 支持,除部分情况 | 全部支持 |
方法增加减少 | 不支持 | 不支持 | 以冷启动方式支持[1] |
方法反射调用 | 只支持静态方法 | 只支持静态方法 | 以冷启动方式支持 |
即时生效 | 支持 | 支持 | 视情况支持[2] |
多DEX | 不支持 | 支持 | 支持 |
资源更新 | 不支持 | 不支持 | 支持 |
so库更新 | 不支持 | 不支持 | 支持 |
Android版本 | 支持2.3~7.0 | 支持2.3~6.0 | 全部支持包含7.0以上 |
已有机型 | 大部分支持[3] | 大部分支持 | 全部支持 |
安全机制 | 无 | 加密传输及签名校验 | 加密传输及签名校验 |
性能损耗 | 低,几乎无损耗 | 低,几乎无损耗 | 低,仅冷启动情况下有些损耗 |
生成补丁 | 繁琐,命令行操作 | 繁琐,命令行操作 | 便捷,图形化界面 |
补丁大小 | 不大,仅变动的类 | 小,仅变动的方法 | 不大,仅变动的资源和代码[4] |
服务端支持 | 无 | 支持服务端控制[5] | 支持服务端控制 |
说明:
- [0] 部分情况指的是构造方法、参数数目大于8或者参数包括long,double,float基本类型的方法。
- [1] 冷启动方式,指的是需要重启app在下次启动时才能生效。
- [2] 对于Andfix及Hotfix 1.X能够支持的代码变动情况,都能做到即时生效。而对于其他代码变动较大的情况,会走冷启动方式,此时就无法做到即时生效。
- [3] Hotfix 1.X已经支持绝大部分主流手机,只是在X86设备以及修改了虚拟机底层结构的ROM上不支持。
- [4] 由于支持了资源和库,如果有这些方面的更新,就会导致的补丁变大一些,这个是很正常的。并且由于只包含差异的部分,所以补丁已经是最大程度的小了。
- [5] 提供服务端的补丁发布和停发、版本控制和灰度功能,存储开发者上传的补丁包。
3.1 原理(双剑合璧)
3.2 优化Andfix(突破底层结构差异,解决稳定性问题)
Andfix底层ArtMethod结构时采用内部变量一一替换,倒是这个各个厂商是会修改的,所以兼容性不好。
Sophix改变了一下思路,采用整体替换方法结构,忽略底层实现,从而解决兼容稳定性问题。
这么一来,不仅解决了兼容性问题,并且由于忽略了底层ArtMethod结构的差异,对于所有的Android版本都不再需要区分,代码量大大减少。即使以后的Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是以线性结构排列,就能直接适用于将来的Android 9.0、9.1等新版本,无需再针对新的系统版本进行适配了。事实也证明确实如此,当拿到Google刚发不久的Android P(9.0)开发者预览版的系统时,hotfix demo直接就能顺利地加载补丁跑起来了,我们并没有做任何适配工作,鲁棒性极好。
3.3 突破QQ和Tinker的缺陷
QZone | Tinker | |
---|---|---|
原理 | 为了解决Dalvik下unexpected dex problem异常而采用插桩的方式,单独放一个帮助类在独立的dex中让其他类调用,阻止了类被打上CLASS_ISPREVERIFIED标志从而规避问题的出现。最后加载补丁dex得到dexFile对象作为参数构建一个Element对象插入到dex-Elements数组的最前面。 | 提供dex差量包,整体替换dex的方案。差量的方式给出patch.dex,然后将path.dex与应用的classes.dex合并成一个完整的dex,完整dex加载得到的dexFile对象作为菜蔬构建一个Element对象然后整体替换掉旧的dex-Elements数组。 |
优点 | 没有合成整包,patch比较小,比较灵活 | 自研dex差异算法,path包很小,dex merge成完整的dex,Dalvik不影响类加载性能,Art下也不存在必须包含父类/引用类的情况 |
缺点 | Dalvik下影响类的加载性能,Art下类地址写死导致必须包含父类/引用,最后patch包很大 | Dex合并内存消耗在VM heap上,容易OOM,最终导致dex合并失败 |
Sophix对dex的解决方案
- Dalvik下采用阿里自研的全量dex方案:不是考虑把补丁包的dex插到所有dex前面(dex插桩),而是想办法在原理的dex中删除(只是删除了类的定义)补丁dex中存在的类,这样让系统查找类的时候在原来的dex中找不到,那么只有补丁中的dex加载到系统中,系统自然就会从补丁包中找到对应的类。
- Art下本质上虚拟机以及支持多dex的加载,Sophix的做法仅仅是把补丁dex作为主dex(classes.dex)而已,相当于重新组织了所有的dex文件:把补丁包的dex改名为classes.dex,以前apk的所有dex依次改为classes2.dex、classes3.dex ... classesx.dex。
dex的merge图解:
3.3 资源修复另辟蹊径
常用方案(Instant Run技术):这种方案的兼容问题在于替换AssetManager的地方
目前市面上的很多资源热修复方案基本上都是参考了Instant Run的实现。实际上,Instant Run的推出正是推动这次热修复浪潮的主因,各家热修复方案,在代码、资源等方面的实现,很大程度上地参考了Instant Run的代码,而资源修复方案正是被拿来用到最多的地方。
简要说来,Instant Run中的资源热修复分为两步:
- 构造一个新的AssetManager,并通过反射调用addAssetPath,把这个完整的新资源包加入到AssetManager中。这样就得到了一个含有所有新资源的AssetManager。
- 找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager。
其实大量代码都是在处理兼容性问题和找到所有AssetManager的引用处,真正的替换的逻辑其实很简单。
Sophix资源修复方案
Sophix方案没有直接使用Instant Run的技术,而是另辟蹊径,构造了一个package id为0x66的资源包,这个包里只包含改变了的资源项,然后直接在原有AssetManager中addAssetPath这个包就可以了。由于补丁包的package id为0x66,不与目前已经加载的0x7f冲突,因此直接加入到已有的AssetManager中就可以直接使用了。补丁包里面的资源,只包含原有包里面没有而新的包里面有的新增资源,以及原有内容发生了改变的资源。并且,我们采用了更加优雅的替换方式,直接在原有的AssetManager对象上进行析构和重构,这样所有原先对AssetManager对象的引用是没有发生改变的,所以就不需要像Instant Run那样进行繁琐的修改了。
可以说,Sophix的资源修复方案,优越性超过了Google官方的Instant Run方案。整个资源替换的方案优势在于:
- 不修改AssetManager的引用处,替换更快更完全。(对比Instanat Run以及所有copycat的实现)
- 不必下发完整包,补丁包中只包含有变动的资源。(对比Instanat Run、Amigo等方式的实现)
- 不需要在运行时合成完整包。不占用运行时计算和内存资源。(对比Tinker的实现)
所以,我们不要被所谓的“官方实现”束缚住手脚,其实Instant Run的开发团队和Android framework的开发团队并不是同一个团队,他们对于Android系统机制的理解未必十分深入。
3.4 SO修复另辟蹊径
so库的修复本质上是对native方法的修复和替换。
我们知道JNI编程中,native方法可以通过动态注册和静态注册两种方式进行。动态注册的native方法必须实现
JNI_OnLoad
方法,同时实现一个JNINativeMethod[]
数组,静态注册的native方法必须是Java+类完整路径+方法名
的格式。相关阅读:Android NDK开发-JNI基础篇
动态注册的native方法映射通过加载so库过程中调用JNI_OnLoad方法调用完成,静态注册的native方法映射是在该native方法第一次执行的时候才完成映射,当然前提是该so库已经load过。
sophix采用的是类似类修复反射注入方式。把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库,而不是原来so库的目录,从而达到修复的目的。
采用这种方案,完全由Sophix在启动期间反射注入patch中的so库。对开发者依然是透明的。不用像某些其他方案需要手动替换系统的System.load来实现替换目的。
4. 项目集成
移动热修复(Mobile Hotfix),阿里云有详细的接入文档,按照文档一步一步来,很容易就实现了。
集成相关文档:
5. 集成问题
5.1 检查到稳健接入初始化错误
":checkApplication FAILED\r"
"BUILD FAILED\r"
"java.lang.RuntimeException: (检查到稳健接入初始化错误<init error 501>)-> Sophix Stub Application类中不得使用非Android SDK的类: [Lap;]\r"
"\tat com.taobao.sophix.c.j.a(PreCheckApplication.java:110)\r"
"\tat com.taobao.sophix.c.e.a(PatchCommand.java:247)\r"
"\tat com.taobao.sophix.Main.main(Main.java:34)\r"
解决方法:
SophixPatchTool工具的打开高级设置->不勾选初始化检查
参考
4.