这两年Android热修复是个挺时髦的东西,基本分为两派,一个是Hook虚拟机的,一个是Hook ClassLoader的。本文就来聊聊这两类方案,算是我自己的理解了。
先说说Hook虚拟机的,这个技术源于国外的Xposed,又被阿里发扬光大,总体来说是个逼格很高的技术。将Java的函数在虚拟机层面改成一个Native函数,之后这个函数的走向就完全由我们来控制了,我们可以将其指向一个我们精心设计的Native函数,这个函数又可以通过JNI调回到上层的Java函数,在这里我们想怎么调都行,可以随意在前后插入我们自己的代码,或者完全替换掉之前的函数。
这个方案在我看来虽然很不错,不过很难在项目中真正去用。一方面是兼容性的问题,毕竟太底层了,虚拟机升级换代都可能让之前的Hack失效。另一方面技术上风险很大,除非团队里有人是虚拟机方面的专家,否则出了什么问题两眼一抹黑就悲剧了,或者要投入很大精力去解决都是得不偿失的。毕竟是个开源的东西,稳定性值得考量。还有一点就是使用起来不太方便,热修复流程就是给补丁包推送给用户,用户加载这个补丁的Dex,取出里面所有的补丁类,依次调用里面的补丁函数来Hook掉有问题的方法,也就是说这个Hook单位是方法,而不是类。而且由于补丁包里是无法直接引用到正式包中的各种类的,所以还需要用到大量的反射来获取类及类中的成员,性能就不说了,真是挺麻烦的。打补丁会非常费事,简单点的bug还行,改一行代码的事,如果是复杂点的bug要涉及到好几个类那就麻烦了。后来阿里又弄出了一个AndFix,这个我还没研究过,据说可以在Smali层自动比对两个包的差异,然后打出补丁包,具体效果怎么样我还不清楚,但是听起来还是很牛逼的,只是用起来不太踏实。
再来说说ClassLoader的方案,这个方案就比上一个方案稍上层一些了,而且是以类为单位来Hook的。总的来说也是钻了空子,我怎么感觉在Java这里就没有安全可言呢。ClassLoader里有一个DexFile数组,加载类时会依次遍历这些DexFile,找到类之后就会直接返回了,后面的就不看了。这个空子很明显了,如果我们打了一个补丁包,给补丁包插在DexFile数组最前面,那加载时之前有问题的类就被补丁中的类替换掉了。这个办法还是很聪明的,不过会遇到一些问题。补丁包中的类如果引用到了主Dex中的类可能会挂掉,原因是这个补丁类可能在Dexopt时被打上了PREVERIFIED标志,因为Dexopt时当发现某个类里的函数所直接引用到的类都在当前Dex中的话就会给该类打上这个PREVERIFIED标志,表示你不用到别的Dex中去找啦,这样可以提升性能。之前Dexopt的时候因为这个类是在主Dex中的,所以被打上了标志,而现在这个类跑到补丁包中了,如果再跨Dex引用主Dex中的类就会抛出异常。解决的办法就是我们可以让所有的类都在构造函数中引用到另外一个Dex中的类,这样这个类就不会被打上PREVERIFIED标志了。不过这又会产生新的问题,分包的时候就没法分了,因为中间都有引用关系,那就会把他们分到一个Dex里了。为了解决这个问题,就不能在源码里直接引用了,因为分包生成引用关系是根据源码来的,所以我们只能在分包列表生成之后,Dex生成之前给所有class里动态注入一段代码来引用到一个跨包类,通常可以用javassist。总的来说,这个ClassLoader方案稳定性要比虚拟机的方案好很多,修bug的话也方便多了,因为是以类为单位的,我们团队目前用的就是这种方案。