写在开头
从15年开始各技术大佬们开始研究热修复技术,并陆续开源了许多的热修复框架。如Jasonross 的Nuwa,美团的Robust,阿里的Andfix,腾讯的Tinker 等等…均是Android 前辈们夜以继日的成果。而现在热修复被广泛地应用于Android 应用和游戏,运用并理解热修复框架在面试中也是加分项。所以,赶紧学起来吧…
本文以Tinker 作为学习对象,主要讲述各开源框架的对比和记录Tinker 的Demo 集成过程。
开源框架对比
这一节篇幅较长,主要是用自己的话来总结各热修复框架的实现原理。如果只想看Tinker接入实现的同学可跳过本节,进入下一章节。
Nuwa 实现原理:
最早看到热修复框架的相关文章就是Qzone官方的文章,但是Qzone热修复技术的实现代码并没有开源。不过GitHub上有一开源的热修复框架Nuwa,实现原理和其相似。这里我们以Qzone为例进行分析。
Qzone的实现原理是生成差分dex文件,将patch.dex插到dexElements的最前面。但patch.dex中的类patchA.java会引用classes.dex中的类classesB.java,如果这两个类所在的patch.dex和classes.dex不是同一个文件就会报错。
疑惑的Qzone技术大佬发现classes.dex和classes2.dex也不是同一个文件,为啥不报错?于是继续查,发现了这个判断
if(!fromUnverifiedConstant && IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)){
如果被引用者(也就是classesB.java)这个类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验。校验不过就报错了。
那么这个标志是什么时候被打上去的?
在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class进行校验。
怎么校验的?
假设classesB.java类在它的static方法,private方法,构造函数,override方法中直接引用到classesC.java类。如果classesB.java类和classesC.java类在同一个dex中,那么classesB.java类就会被打上CLASS_ISPREVERIFIED标记,被打上这个标记的类不能被其他dex中的类引用,否则就会报错。所以要防止类被打上CLASS_ISPREVERIFIED的标志。
如何防止类被打上CLASS_ISPREVERIFIED的标志?
Common类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的Common类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。
优点:
- 开发透明,简单;
- 热修复成功率高。
缺点:
- 在Art 上表现为补丁包较大;
- 在Dalvik 上性能消耗较大。
Robust实现原理:
Robust 插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码。通过判断 if(changeQuickRedirect != null)
来确定是否进行热修复,当 changeQuickRedirect 不为 null 时,调用 patch.dex 中同名类的同名方法达到 hotfix 的目的。
如何调用呢?
生成的patch.dex 中有两个主要类,PatchesInfoImpl.java 和修复后的同名类 APatch.java。客户端拿到patch.dex 后,用DexClassLoader 加载patch.dex,反射拿到PatchesInfoImpl.java 这个 class。并创建这个class 的一个对象。然后通过这个对象知道被替换的是谁,给它的变量changeQuickRedirect 赋值为patch.dex中的APatch的对象,这样就会去执行补丁包中的方法了。
大致流程图如下所示:A_old表示未被修复的原有类,A_new表示已修复的新类。
优点
- 兼容性高,开发透明;
- 实时生效。
缺点
- 会增大方法数,影响运行效率;
- 暂不支持 so 文件和资源的替换。
Andfix实现原理:
Andfix 采用的方法是,在已经加载了的类中直接在 native 层替换掉原有方法,是在原来类的基础上进行修改的。其核心在于 replaceMethod 函数,所以只支持方法替换,对于方法的增删,资源更新,so 文件更新及类和属性的替换等都是不支持的。
Andfix 的实现偏 native 层,笔者能力有限,其具体实现过程,就不妄加总结了。
更多实现细节请看 Andfix 官网文章
优点
- 立即生效,消耗低;
- 补丁包较小。
缺点
- 仅支持方法替换;
- 兼容性不佳,对部分机型暂不支持。
Tinker实现原理:
在 App 运行到一半的时候,所有需要发生变更的 Class 已经被加载过了,在Android 上是无法对一个 Class 进行卸载的。而Tinker的方案,都是让 Classloader 去加载新的类。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会 Resolve 为新的类。从而达到热修复的目的。
Tinker 的实现过程更像是在 Qzone 热修复方案上做优化。核心点是性能最优,消耗最低。
经 Tinker 开发人员调研,Qzone 的方案最大挑战在于性能,即Dalvik平台存在插桩导致的性能损耗,Art平台由于地址偏移问题导致补丁包可能过大的问题。为了避免这两个问题,根据 Instant Run 的全量替换新的 Dex 的思路,于是决定将新旧两个Dex 的差异放到补丁包中。
经过调研,BsDiff 算法对 Dex 支持效果不太好,所以,Tinker 开发团队人员自研了 DexDiff 算法。
最终, BsDiff 加载 so 和部分资源文件,DexDiff 加载 Dex文件,以达到性能最优。但是这个方案也有缺点,就是占用 ROM 较大。好吧!现在手机内存都不小,多几十 M 可以接受。
优点
- 补丁包较小,消耗较小;
- 开发透明,文档丰富。
缺点
- 占用 ROM 较大;
- 需要重启才能生效。
对比与选择:
Type | Nuwa | Robust | Andfix | Tinker |
---|---|---|---|---|
Company | Null | Meituan | Alibaba | Tencent |
开发时间 | 2015 | 2016 | 2015 | 2016 |
替换类 | √ | X | X | √ |
替换So | X | X | X | √ |
替换资源 | √ | X | X | √ |
即时生效 | X | √ | √ | X |
成功率 | 较高 | 最高 | 一般 | 较高 |
接口文档 | ★★ | ★★ | ★★ | ★★★★ |
单就热修复线上 APP 某一处或多处 bug 来说,Andfix能做到即时修复,且操作简单,不用生成较多的 patch.dex 包,能轻松解决紧急问题。
但对于如非紧急 bug 的修复及小版本的发布,对即时生效性要求不高的情况,Tinker 支持的替换内容较丰富,更胜一筹。
阿里将 Andfix 升级为商业版 SDK Sophix;腾讯将 Tinker 升级为 Bugly。
Sophix 不但支持即时修复,还支持再次启动修复类、so 文件、资源等。作为商用 APP 集成 Sophix 是很好的选择。
但 Tinker 的开源,为其带来了大量的使用者和测试者,除此以外还与各大手机厂商建立联系,使得各厂商在系统定制时也会考虑是否影响热修复的问题。所以 Tinker 的兼容性可见一斑。
总的来说,各有千秋,各需所需吧。
我们这里以 Tinker 为学习对象,接下来先让 Tinker-Demo 跑起来,看一下实际效果。
Tinker-Demo 效果
下载 Github 上的开源代码,然后仅需导入 tinker-sample-android 工程即可。
添加依赖
在项目的 build.gradle 中,添加 tinker-patch-gradle-plugin 的依赖
buildscript {
dependencies {
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')
}
}
然后在app的gradle文件app/build.gradle,我们需要添加tinker的库依赖以及apply tinker的gradle插件.
dependencies {
//可选,用于生成application类
provided('com.tencent.tinker:tinker-android-anno:1.9.1')
//tinker的核心库
compile('com.tencent.tinker:tinker-android-lib:1.9.1')
}
...
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
准备好后,运行…
看一下app/build.gradle中在哪里设置tinkerId。
def getTinkerIdValue() {
return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}
def gitSha() {
String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
...
这里设置成版本号即可
String gitRev = '1.9.1'
再运行…
点击 SHOW INFO 按钮
生成补丁包
MainActivity.java中添加代码
Toast.makeText(this, "hello, Tinker", Toast.LENGTH_SHORT).show();
在app/build.gralde中,将刚才生成的apk包标记为oldApk
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
...
oldApk = getOldApkPath()
def getOldApkPath() {
return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
包名改成和左边的一样。
在底部Terminal中输入生成补丁包的命令 graldew tinkerPatchDebug
…
报错
com.tencent.tinker.build.util.TinkerPatchException:
Warning: ignoreWarning is false, manifest was changed, while hot plug component support mode is disabled. Such changes will not take effect.
搜了下Issues,有相同的问题。官方技术大佬是怎么回复的,不过具体原因还有待研究…
ignoreWarning = true
这里设置为忽略警告,再次 graldew tinkerPatchDebug
成功之后有个patch_signed_7zip.apk
下载并合成补丁
可以使用命令行将补丁包发送到手机。
adb push ./app/build/outputs/apk/tinkerPatch/debug/patch_signed_7zip.apk /storage/sdcard0/
不过这里运行失败了
adb server is out of date. killing...
CreateProcess failure, error 2
* failed to start daemon *
error:
试了一大堆方法,无果…
好吧!手动拷贝到手机文件管理根目录下。
再次打开Tinker-Demo
点击LOAD PATCH 按钮
过了2-3s 出现Toast 提示
返回,再进入…
没反应
这里注意,必须要杀掉进程,再次进来才能成功加载patch包的代码。
手动杀掉,或者点击KILL SELF 按钮
完成修复
写在后头
各个框架各有优劣,Tinker 官方在文档中也指出其不足之处:
Tinker经过几次全量上线,也发现了一些热补丁的问题。有以下的一些优化工作尚未完成:
1. 支持四大组件的代理;
2. Crash 启动保护;
道阻且长,目前Tinker 还只能是替换类、资源和so 文件等,如果支持了四大组件的代理,也许所有的非重大版本更新都可以用热修复来实现了。
记录在此,仅为学习!
感谢您的阅读!欢迎指正!
欢迎加入 Android 技术交流群,群号:155495090
参考文章
- Qzone-安卓App热补丁动态修复技术介绍 (https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a)
- 美团点评Robust (https://tech.meituan.com/android_robust.html)
- Android热修复升级探索 (https://yq.aliyun.com/articles/74598?spm=5176.100239.blogcont103527.13.9ka3bq#1)
- Tinker – 微信Android热补丁方案 (https://github.com/Tencent/tinker/wiki)