App发布出去后,如果发现有紧急或重要bug如何进行修复呢?
重新发布一版APK?但这样代价太大....
那么有没有一种方案能够不用更新整个APK,而只把服务器上的很小的补丁文件下载下来进行修复bug呢?
本文的调研也正是为了解决该问题。
下面是找到的3款比较火的开源解决方案,分别都是利用不同原理实现的:
名称 |
优点 |
缺点 |
适用场景 |
实现原理与下载 |
DynamicAPK |
1.支持插件化开发,并且Plugin APK能够访问Host APK的资源 |
1.需要修改aapt工具,改变原来的编译流程。 2.不支持Hot fix(官方介绍有文字提到支持,但只是load的时候考虑了下,还有很多工作没有实现)
|
适用于Plugin开发 |
【实现原理】 这是携程网开源的,Multidex+AAPT编译流程改造。参考:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading?utm_campaign=infoq_content& 【源码下载】 https://github.com/CtripMobile/DynamicAPK
|
AndFix |
1.与Mexdex方案相比,性能要好些。(Multi Dex需要修改所有class的class_ispreverified标志位,导致运行时性能有所损失)
|
1.跳过了类初始化,对于静态或者构造函数或者class.forname()的处理可能会有问题 |
在线修复bug |
【实现原理】 阿里的开源项目,原理是函数Hook。GitHub上有介绍。 【源码下载】 https://github.com/alibaba/AndFix |
Nuwa (HotFix/DroidFix)
|
1.兼容性比AndFix好(Multi dex方案,没有static的问题) 2.支持ART与Dalvik 3.支持6.0
|
1.编译完成java代码后,需要遍历修改class文件,插入代码,防止class被打上class_ispreverified标志,这会导致运行性能降低 |
|
【实现原理】 Multidex的动态加载原理,参考:http://bugly.qq.com/blog/?p=781 【源码下载】 |
结合我的目标,目标锁定在第2个与第3个。
选择第二个AndFix来进行研究:
第一步:模拟发布APK:
使用自带的sample中的列子,修了下onCreate()中的代码,如下:
安装运行bug.apk,如预期输出如下Log:
第二步:现在App发布出去后有Bug啦,我们要改变onCreate()的输出,赶紧开始制作patch.....
修改输出代码为如下:
编译出fix后的APK,这时需要使用AndFix/tools/下面的apkpatch这个工具来制作patch(补丁)文件。
工具所在目录:
工具使用说明:
ApkPatch v1.0.3 - a tool for build/merge Android Patch file (.apatch).
Copyright 2015 supern lee
usage: apkpatch -f -t -o
执行如下命令:
apkpatch.sh -f fixed.apk -t bug.apk -o out -k sig -p 123123 -a test_sig -e 123123
命令的输出:
add modified Method:V onCreate(Landroid/os/Bundle;) in Class:Lcom/euler/andfix/MainActivity;
第三步:到此补丁文件制作好了,就可以通过网络等渠道推送到手机上。
查看out目录,一共有3个,会在后面进行解释:
这里的.patch文件就是我们需要的,把它push到手机:
重起刚才的APP,观看Log输出时否改变了:
^_^,补丁成功了,而且这个文件也才3KB多点哦,非常适合在线修复bug~~~~
-rw-rw-r-- 1 yanchen yanchen 3468 12月 15 18:32 out/fixed-07d99a18833f092518fbb041c793e53b.apatch
============================================================================================================================
Patch的制作流程现在知道了,下一步就准备进入Code层面的分析,探究它的内部实现原理。
============================================================================================================================
首先来看Patch制作原理:
制作patch会用到apkpatch这个工具,而它会调用apkpatch-1.0.3.jar这个文件。反编译这个jar包,发现在制作patch时,它会进入ApkPatch的doPatch()这个函数完成的。
进入doPatch函数看看:
这个函数逻辑挺清晰的,一共就这4个步骤。
第一步是diff,它会比对两个APK的差别,来看看它的代码是如何实现的:
这个函数会提取fixed.apk与bug.apk中的classes.dex文件,然后通过2个for循环,来对比每个class文件中的字段与函数是否完全一致。
那么字段和函数又是如何对比的呢?
我们来看看compareMethod()函数内部实现:
原来对比函数时会看两个是否有函数实现,有的话就会看两个函数的函数体是否一致,代码如下:
如果不一致就会添加到一个叫info的HashSet变量中。
而比对字段主要看字段的初始值是否一样,代码如下:
总结:
通过上面的步骤就比对出了两个APK中的classes.dex中差别,并且将有差别的文件保存在了info变量中。
接下来进入第二步,开始进行buildCode(),这步的目的是将info中保存的文件写到.smali文件中,然后再打包成一个dex文件。
比如下面的是我的demo产生的smali文件:
在生成的smali函数中,它会添加一个自定义的Anotation,叫MethodReplace:
到此,第二个步骤也就完成了,它一共产生2个文件:
接下来进入第三个步骤,调用build()函数,这步会将上一步产生的dex文件写入到一个jar文件,并进行签名,代码如下:
PatchBuilder的代码如下:
所以第三步完成后,会产生一个叫作diff.apatch的jar文件。
接下来进入最后一步,调用release()函数,也就是通过它产生最终的patch文件。
看看release()中都做了些什么事情:
主要是将第三步生成的diff.apatch文件重命名为name-md5-.patch格式的文件。
到此,patch的制作原理也就告一段落了,主要就是提取两个APK中的classes.dex,对比他们中的class文件是否有区别,将有区别的提取出来打包到.apatch文件中。
最终的产物如下:
============================================================================================================================
以上是Patch的制作流程分析,下一下我们再来看看客户端是如何将这个patch文件打入自己的APK中的。
============================================================================================================================
那我们就来看看这几行代码到底会做什么事情。
init()函数主要工作时载入files/apatch/目录下的.apatch文件,代码如下:
initPatchs()代码如下:
它读取mPathDir目录下的所有.patch文件,并将起添加到一个叫mPatchs的HashSet变量中。
这是mPatchDir的初始值:
看来这个函数的目的就是在启动的时候,读取所有本地的patch文件。
读取完毕后调用loadPatch()来进行运行时的APK修复,代码如下:
fix()函数的代码如下:
它会加载.apatch文件中class,然后再调用fixClass(),继续往下看:
会读取自定义的Anotation MethodReplace,通过它获取到class名字与method名字,进行替换。
在前面的分析中也介绍过MethodReplace的内容格式,可参考如下:
而在进行Replace Method是,是在Native层做:
在JNI的代码中,支持Dalvik与ART,这时它的代码结构:
其修复原理就是从内存中找出原来函数指令指针,让它指向新的函数地址:
上面的meth变量便是我们bug.apk中的函数的句柄,target便是.aptach文件中函数的句柄。
而insns是函数指令地址的指针,解释如下:
到此函数替换的原理就水落石出了,就是函数Hook。
很犀利的做法。
所以总结一下补丁执行的原理就是:
在运行时,读取patch文件中的函数,将它的函数指令地址赋给APK中的函数。
这样不就等于替换了原来的函数么?那么bug也就可以被消除了。。。
当然这一切都是在内存中进行的,不会对本地的APK有任何影响。