Android热修复框架学习及应用

写在开头

从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的标志了,只要没被打上这个标志的类都可以进行打补丁操作。 
更多实现细节请看 安卓App热补丁动态修复技术介绍

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表示已修复的新类。

开始changeQuick-Redirect != nullDexClassLoader 加载 patch.dex反射获取 PatchesInfoImpl 对象获取被修复的类名及方法名反射得到 A_old 的对象给 A_old 类中的 changeQuick-Redirect 对象(此时为null)赋值为A_new类的对象执行A_new类中的代码结束执行原有代码yesno

更多实现细节请看 Robust官网文章

Andfix实现原理:

Andfix采用的方法是,在已经加载了的类中直接在native层替换掉原有方法,是在原来类的基础上进行修改的。其核心在于replaceMethod函数。 
更多实现细节请看 Andfix官网文章

Tinker实现原理:

在app运行到一半的时候,所有需要发生变更的Class已经被加载过了,在Android上是无法对一个Class进行卸载的。而Tinker的方案,都是让Classloader去加载新的类。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve为新的类。从而达到热修复的目的。 
更多实现细节请看 Tinker官方文章

对比与选择:
Type Nuwa Robust Andfix Tinker
Company Null Meituan Alibaba Tencent
开发时间 2015 2016 2015 2016
替换类 X X
替换So X X X
替换资源 X X
即时生效 X X
成功率 较高 最高 一般 较高
接口文档 ★★ ★★ ★★ ★★★★

综上所述,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')
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

然后在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'
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

准备好后,运行…

tinkerId is not set!!!

看一下app/build.gradle中在哪里设置tinkerId。

tinkerId

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}
 
 
  • 1
  • 2
  • 3
def gitSha() {
        String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
        ...
 
 
  • 1
  • 2
  • 3

Tinker常见问题文档

tinkerId is not set 官网回答

宁波整形医院http://www.zuanno.com/
宁波最好的整形医院http://www.zuanno.com/

这里设置成版本号即可

String gitRev = '1.9.1'
 
 
  • 1

再运行… 
点击 SHOW INFO 按钮

这里写图片描述

生成补丁包

MainActivity.java中添加代码

Toast.makeText(this, "hello, Tinker", Toast.LENGTH_SHORT).show();
 
 
  • 1

在app/build.gralde中,将刚才生成的apk包标记为oldApk

if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'
    tinkerPatch {
    ...
        oldApk = getOldApkPath()
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
 
 
  • 1
  • 2
  • 3

oldApkPath

包名改成和左边的一样。 
在底部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.
 
 
  • 1
  • 2

搜了下Issues,有相同的问题。官方技术大佬是怎么回复的,不过具体原因还有待研究…

官方技术大佬的回复

ignoreWarning = true 这里设置为忽略警告,再次 graldew tinkerPatchDebug

成功之后有个patch_signed_7zip.apk

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 提示

Toast 提示

返回,再进入… 
没反应 
这里注意,必须要杀掉进程,再次进来才能成功加载patch包的代码。 
手动杀掉,或者点击KILL SELF 按钮

hello, Tinker

完成修复

写在后头

各个框架各有优劣,Tinker 官方在文档中也指出其不足之处:

Tinker经过几次全量上线,也发现了一些热补丁的问题。有以下的一些优化工作尚未完成: 
1. 支持四大组件的代理; 
2. Crash 启动保护;

道阻且长,目前Tinker 还只能是替换类、资源和so 文件等,如果支持了四大组件的代理,也许所有的非重大版本更新都可以用热修复来实现了。

记录在此,仅为学习! 
感谢您的阅读!欢迎指正!

参考文章

  1. Qzone-安卓App热补丁动态修复技术介绍 (https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a)
  2. 美团点评Robust (https://tech.meituan.com/android_robust.html)
  3. Android热修复升级探索 (https://yq.aliyun.com/articles/74598?spm=5176.100239.blogcont103527.13.9ka3bq#1)
  4. Tinker – 微信Android热补丁方案 (https://github.com/Tencent/tinker/wiki)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值