Android热修复框架AndFix原理解析及使用
一、前言
最近腾讯弄出一个Tinker热修复框架,那么本文先不介绍这个框架,先来介绍一下阿里的一个热修复框架AndFix,这个框架出来已经很长时间了,但是看网上没有太多非常详细的讲解,这里就来做一次分析。正好项目中要使用到。首先这个框架是开源的:https://github.com/alibaba/AndFix 其实在最早的时候我已经分析了阿里的另外一个热修复框架:Dexposed框架,还不了解的同学可以点击这里查看:Dexposed框架原理解析以及使用 当时介绍这个框架的时候发现他的实现原理很简单:
他的思想完全来源于Xposed框架,完美诠释了AOP编程,这里用到最核心的知识点就是在native层获取到指定方法的结构体,然后改变他的nativeFunc字段值,而这个值就是可以指定这个方法对应的native函数指针,所以先从Java层跳到native层,改变指定方法的nativeFunc值,然后在改变之后的函数中调用Java层的回调即可。实现了方法的拦截功能。
二、源码分析
那么本文介绍的AndFix框架相对于Dexposed框架来说又有什么区别呢?其实区别就在于AndFix框架更加轻便好用,在进行热修复的过程中更加方便了。当然这个优点在后面的Tinker框架也是能提现出来的。这个框架的原理是:直接在native层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能。下面通过分析他的源码来看他的具体实现,下载完源码之后导入工程:
这里可以看到,因为在native层需要替换新旧方法结构体信息,所以这里肯定要做的工作就是虚拟机的兼容问题,这里做了art和dalvik的分开处理逻辑,下面来看一下这个框架的基本使用规则:
用法还是很简单的,这里的修复包是直接放在本地的,在实际操作中会从网上去下载。下面就开始分析源码,这里有一个主要的类就是PatchManager:
第一、PatchManager类初始化
在这个类的构造方法中做了两件事,一件事是初始化AndFixManager类,一件事是创建修复包存放的沙盒目录。这里先来看第二件事创建沙盒目录:
可以看到这个目录是:/data/data/xxx/files/apatch/xxx.apatch
当我们从网上下载好修复包apatch文件之后,会调用addPatch方法,这时候会把修复包复制到这个地方,以后再次启动时就会遍历这个目录加载apatch文件。
第二、AndFixManager的初始化
下面继续来看AndFixManager初始化操作:
在这个初始化中也是干了两件事:一件事是判断当前环境是否支持热修复,一件事是初始化修复包安全校验的工作,先来看一下判断是否支持操作:
这里支持的条件是:非YunOS系统,Android2.3-7.0系统版本,热修复native层设置是否成功。这里我们看到应该值得关心的是setup操作,这个操作其实是native层进行的,可以直接看一下具体代码:
这里主要做了一些初始化操作,获取一些函数指针,准备后续的replaceMethod函数中使用:
1、在libdvm.so动态获取dvmDecodeIndirectRef函数指针和获取dvmThreadSelf函数指针。2、调用dest的 Method.getDeclaringClass方法获取method的类对象clazz。
继续回到AndFixManager的初始化中的第二件事:修复包安全校验工作,其实这里只是做了初始化操作,而真正校验的工作在后面,主要是通过比对应用的签名和修复包的签名信息。后面再看吧!
第三:PatchManager的初始化操作
这里的初始化做了一件事:是判断当前PatchManager的版本号是否发生变化,如果发生变化就清空本地所有的修复包。如果没有变化,直接调用初始化修复包方法
这个方法其实就是我们上面提到的逻辑,会遍历沙盒中修复包目录中所有的修复包文件,然后把它添加到修复列表中。
第四:PatchManager的添加修复包操作
这里提供了两个添加修复包的方法,一个是接受文件样式的参数:
这个方法就是上面的那个initPatchs方法中调用的地方,这里会把目录下所有的修复包文件加到列表中。这里来看一下另外一个主要类Patch的初始化操作:
这个初始化的主要工作就是通过JarFile类解析修复包文件,读取他的META-INF\PATCH.MF文件内容,获取需要修复类的名称,多个修复类之间用逗号分隔,类似于这样的样式:
这里的Utils类就是我们需要修复的方法所属的类名,但是这里他又做了处理就是在每个类的后面加了后缀:_CF。这里分析完了所有需要修复的类名之后保存到一个列表中,后面会通过修复包名称获取到他的修复类名称列表。
还有一个添加修复包文件的方法,接受的是文件路径参数:
这个方法接受的是修复包文件路径,首先会把这个文件拷贝到上面提到的沙盒目录中以便下次进行遍历操作,拷贝之后继续调用上面的addPatch方法添加到列表中,最后就在调用加载修复包操作了。
注意:
第一个方法接受文件样式的方法其实是需要结合上面的initPatchs方法一起使用,他调用的场景是:本地沙盒目录中已经有了修复包文件,并且版本号没有发生变化,这样每次启动程序的时候就会调用初始化操作,在这里会遍历沙盒目录中所有的修复包文件,然后调用这个方法添加到全局文件列表中。
第二个方法接受的是文件路径样式,这个方法使用的场景是版本号发生变化,或者是本地沙盒中没有修复包文件。比如第一次操作的时候,会从网络上下载修复包文件,下载成功之后会把这个文件路径通过这个方法调用即可,执行完之后也会主动调用加载修复包的操作了,比如这里第一次在SD卡中放了一个修复包文件:
只要调用了这段代码之后就可以走完了所有的流程了:拷贝修复包到沙盒目录中,加载修复包文件。
第五:PatchManager的加载修复包操作
这个方法有两个地方会调用到:一个是上面提到的那个接受修复包路径的addPatch方法,一个是调用完接受修复文件类型的addPatch方法之后手动调用一次,类似于这样:
这个方法内部主要是通过Patch类获取修复包所有的修复类名称,之前已经介绍了Patch类的初始化操作,在哪里会解析修复包的MF文件信息,获取到修复包需要修复的类名然后保存到列表中,这里就通过getClasses方法来获取指定修复包名称对应的修复类名称列表,然后在调用AndFixManager的fix方法即可,下面再来看一下AndFixManager的fix方法的实现逻辑:
这个方法有点长,而且内容也比较多,这里主要做了这么几件事:
第一件事:使用上面初始化完成的校验类进行修复包的校验工作,这里的校验就是比对修复包的签名和应用的签名是否一致:
这个具体实现逻辑不用介绍了,大家可以下载源码自己分析。
第二件事:使用DexFile和自定义类加载器来加载修复包文件
这个其实和使用DexClassLoader加载原理类似,而且DexClassLoader内部的加载逻辑也是使用了DexFile来进行操作的,而这里为什么要进行加载操作呢?因为我们需要获取修复类中需要修复的方法名称,而这个方法名称是通过修复方法的注解来获取到的,所以咋们得先进行类的加载然后获取到他的方法信息,最后通过分析注解获取方法名,这里用的是反射机制来进行操作的。
这个加载完类之后就会继续调用fixClass方法,再来看一下fixClass方法实现:
这里主要是通过反射获取指定类名需要修复类中的所有方法类型,然后在获取到他的注解信息,上面已经分析了通过DexFile加载修复包文件,然后在加载上面Patch类中的getClasses方法获取到的修复类名称列表来进行类的加载,然后在用反射机制获取类中所有的方法对应的注解信息,通过注解信息获取指定修复的方法名称,看一下这个注解的定义:
这里提供了两个方法,一个是获取当前类名称,一个是获取当前方法名称,可以看一下具体事例:
上面解析完注解信息之后获取到了方法名称,紧接着就调用了replaceMethod方法开始了方法的替换操作
这里还会做一件事就是通过上面得到的修复新的方法信息以及需要修复的旧方法名称来操作,不过这里得先获取到旧方法类型,可以看到修复的新旧方法的签名必须一致,所谓签名就是方法的名称,参数个数,参数类型都必须一致,不然这里就报错的。进而也修复不了了。最后在调用了AndFix的addReplaceMethod方法进行native层的修复工作:
这里会做虚拟机的区分处理,但是他们大致的处理逻辑都是一致的,这里来看一下dalvik的处理机制:
这里的操作也是非常简单的,主要是通过上层传递过来的新旧方法类型对象,通过JNIEnv的FromReflectedMethod方法获取对应的方法结构体信息,然后将其信息进行替换即可,这里可以看到替换的信息也是非常多的,而且也看到了我们之前介绍Dexposed框架用到的一个字段值nativeFunc,这个就是指定这个Java方法对应的native方法。但是在这之前也会看到有一段代码是用来获取修复方法的类信息的,这里主要是用来做修复方法的类初始化操作,在之前我们看setup方法的时候知道,那里做了这么两件事:
1、在libdvm.so动态获取dvmDecodeIndirectRef函数指针和获取dvmThreadSelf函数指针。2、调用dest的 Method.getDeclaringClass方法获取method的类对象clazz。
然后在这里就开始获取修复方法对应的类信息,通过调用方法的getDeclaringClass获取方法对应的类对象clazz,然后在调用dvm方法获取到对应的类结构体信息ClassObject,最后在设置他的状态信息标记这个类已经初始化完毕了。
注意:
这里可以看到通过一个类对象clazz类型可以获取到对应的结构体信息ClassObject,这个操作也是非常实用的,因为这个结构信息中有一个字段pDvmDex值,而这个值就是DvmDex结构体信息也就是底层对应的dex文件信息,所以说我们可以通过一个类信息得到他对应的dex文件信息。
三、流程总结
到这里就讲解完了整个框架的所有技术点了,上面可能说的有点乱,下面在来大体总结一下,首先来看一张简单的流程图信息(图片有点大,可以下载看高清大图):
第一、Patch类负责解析每个修复包apatch文件信息,获取所有需要修复的类名
这个类的初始化操作中会通过传递进来的修复包文件,使用JarFile类进行文件解析,读取他的META-INF\PATCH.MF文件信息,主要通过读取Patch-Classes字段值来获取需要修复的类名称,多个类名称之间用逗号分隔,而且每个类名称都有一个后缀:_CF。解析完成之后就会保存到一个用修复包名称作为key的HashMap中,后面会通过修复包名称参数来调用getClasses方法获取对应修复的类名称列表。
第二、PatchManager负责管理多个Patch类也就是多个修复包信息
主要方法包括初始化,添加修复包,加载修复包,这个类是提供给外界调用的一个入口类,这里有两种方式调用:
一种方式是先调用init方法进行初始化,在这个初始化方法中会判断当前的版本号,如果版本号发生变化就会清空本地所有的修复包文件,如果没有变化就加在所有的本地修复包文件,而这个本地目录就是沙盒中存放修复包文件的目录:/data/data/xxx/apatch/xxx.apatch,然后在调用loadPatch方法进行修复包的加载工作。
一种方式是本地没有修复包文件,也就是第一次操作的时候可能需要从服务器下载修复包文件,这时候会把下载下来的修复包文件路径通过调用addPatch方法进行添加操作,而这里的添加操作包括了:先把修复文件拷贝到上面的沙盒修复包目录中,然后在调用loadPatch方法进行加载工作。
第三、AndFix类主要是和native层交互直接替换方法
这个类主要就是几个native方法用来和底层进行交互的操作,而这些方法都是会被AndFixManager进行调用的。
第四、AndFixManager类主要是负责管理AndFix类
主要方法包括加载每个修复包中需要修复的类,解析出每个类的注解信息获取该类需要修复的方法名称,初始化的时候会进行修复包的校验工作,主要通过对比修复包和应用的签名信息。所以可以知道每个修复包是需要进行签名操作的,然后他的fix方法会使用DexFile类进行加载修复包文件,调用Patch的getClasses方法获取到所有需要修复的类名称进行加载操作。然后在调用fixClass方法,在这个方法中主要通过遍历修复类中所有指定MethodReplace注解信息的方法信息,然后在调用replaceMethod方法进行替换操作,而在这个方法中也会通过新方法的Method类型和注解信息中需要修复方法的名称来得到旧方法的Method类型,最终调用AndFix的native方法replaceMethod进行替换操作,所以这里可以看到替换的新旧方法的签名信息必须一致,不然无效,也就是方法的名称,参数个数,参数类型必须保持一致才可以。
第五、Native层方法
在native层中会做art和dalvik虚拟机的区分处理工作,他们大致的逻辑都是一致的:
dalvik 模式下的Java hook1、在libdvm.so动态获取dvmDecodeIndirectRef函数指针和获取dvmThreadSelf函数指针。2、调用dest的 Method.getDeclaringClass方法获取method的类对象clazz。3、调用dvmDecodeIndirectRef方法,获取clazz的ClassObject4、通关 env->FromReflectedMethod方法获取dest的Method结构体函数的指针5、替换method结构体的成员数据art模式下的java hook1、art模式中,我们直接通过 env->FromReflectedMethod获取到ArtMethod函数指针。2、然后直接替换ArtMethod结构体的成员数据指针
******四、框架使用案例*
上面介绍完了原理,下面如果不用案例来做分析,那都是白扯淡,这里我们就用一个简单的案例来进行实际操作一下,而且在这个过程中会发现一个神奇的工具apatch,这里的例子很简单,本地定义一个获取版本号的方法:
这时候我们得到这个值,然后显示在界面上,然后开始出release包,这里直接用eclipse构造签名文件(这个签名文件要记得保存好,后面会使用到)出包了。等包上线发布之后,突然发现这个版本号错了,应该是1.0.2,那么这时候就需要进行热修复了,操作很简单:
第一步:修改这个方法返回值为1.0.2
第二步:继续使用上面的签名文件进行签名得到了一个修复之后的apk包
第三步:使用神器apatch进行线上发布的release包和这次修复的fix包进行比对,获取到修复文件apatch
java -jar apkpatch.jar -f app-release-fix.apk -t app-release-online.apk -o C:\Users\jiangwei1-g\Desktop\apkpatch-1.0.3 -k jiangwei.keystore -p 123456 -a jiangwei -e 123456
这里在使用命令的时候需要用到签名文件,因为在前面分析代码的时候知道会做修复包的签名验证。这里得到了一个修复包文件如下:
而且会产生一个diff.dex文件和smali文件夹,这个就是修复类的文件对应的dex文件和smali代码:
而我们用压缩软件可以打开apatch文件看看:
可以看到这里的classes.dex文件其实就是上面的diff.dex文件,只是这里更像是Android中的apk文件目录格式,同样有一个META-INF目录,这里存放了签名文件以及需要修复类信息的PATCH.MF文件:
签名文件就不多说了,来看一下PATCH_MF文件信息:
Patch_Classes字段包含了需要修复的类的名称信息了。
第四步:这里为了演示方便,直接把上面的修复文件拷贝到sd卡中,然后调用PatchManager的addPatch方法:
第五步:运行程序
这时候可以发现版本号已经修复成了1.0.2了。
五、apatch工具原理解析
上面看到案例使用比较简单,但是看到有一个比较牛逼的工具就是apatch,可以生成有方法变动的类所在的dex文件,那下面就来一起看看他的实现原理,没找到源码,直接使用jd-gui查看apatch.jar文件了:
这里的核心代码就是这部分,会把有方法变动的类信息列表对象DexBackedClassDef借助baksmali类写入到smali文件中,然后在借助DexBuilder和SmaliMod类把smali类变成dex文件,也就是最终的diff.dex文件了。那么下面在来看一下这个变动的DexBackedClassDef类列表信息如何得到的:
在这里使用DexBackedDexFile类进行加载新旧的dex文件,然后开始比对具体方法实现变动情况,主要是方法:compareMethod的实现:
这里会调用方法的getImplementation方法来判断新旧方法的实现发生变动了,如果有就把当前的类对象加入变动列表中即可。
所以从这里可以看到这里其实是完全借助了第三方的功能:可以把dex变成smali文件的baksmali工具包、可以把smali变成dex文件的smali工具包。而这两个工具包的源码之前在介绍apktool反编译工具的时候说到了,想查看源码的同学可以查看这篇文章:反编译利器apktool的源码解析。从这里可以看到,我们后续再处理dex,smali等文件格式的时候这两个工具包用的非常多。
六、框架技术总结
到此此次修复操作就完成了。我们的讲解工作和案例演示工作也完成了,下面来总结一下这个框架的知识点,不过先来看一张大图(可以点击下载查看高清大图):
第一、核心技术点
从上面可以看到AndFix框架的技术点主要包括:
1、使用apatch工具生成修复包文件,主要借助baksmali和smali工具包实现
2、Java层传递新旧方法类型对象,到native层获取其对应的结构体信息实现完美替换新旧方法结构信息
第二、优点和局限性
优点:从上面可以看到这个框架的优点在于轻巧便捷,集成成本低,维护性强。
局限性:从上面的代码分析可以看到这个框架的局限性还是很多的,特别是他只能修复对应已经存在的方法,比如现在我想增加一个方法肯定不行的,如果想给修复方法增加参数信息也是不可以的,这个局限性就非常大了。还有一个局限性就是只能进行代码修复,资源是无法做到的。所以从这里可以看到这个框架更偏重于方法的热修复操作。
项目下载地址:http://download.csdn.net/detail/jiangwei0910410003/9678441
工具下载地址:http://download.csdn.net/detail/jiangwei0910410003/9678885
七、总结
在开发过程中现阶段热修复技术还是很火的,而一些大公司也相继给出了一些热修复的合理方案,每家都有各自的优点和缺点,而我就要做到每家热修复框架的详细原理解析,从中能够学习到更多的技巧和知识,顶着头疼的风险写完了这篇文章,记得多点赞多分享!
更多内容:点击这里
关注微信公众号,最新技术干货实时推送
******扫一扫加小编微信****添加时注明:“编码美丽”否则不予通过!**