前段时间 IOS 取消了相关框架的热更,在 CSDN 社区专家群引起了大量的讨论。大家纷纷发表自己的看法和技术解决方案。本文不是 IOS 相关的问题的解决方案,主要围绕 Android 的热更新进行学习!
综合前面的几篇文章《Android热修复升级原理和实践》、《Android热修复升级、兼容性问题的根源》、《详解 Android 热更新升级如何突破底层结构差异?》。
看了前面的几篇文章,你可能会有疑惑:我们只是替换了ArtMethod的内容,但新替换的方法的所属类,和原先方法的所属类,是不同的类,被替换的方法有权限访问这个类的其他private方法吗?
以这段简单的代码为例
public class Demo{ Demo{ func(); } private void func(){} }
Demo构造函数调用私有函数func所对应的Dex Code和Native Code为
这个调用逻辑和之前Activity的例子大同小异,需要注意的地方是,在构造函数调用同一个类下的私有方法func时,没有做任何权限检查。也就是说,这时即使我把func方法的偷梁换柱,也能直接跳过去正常执行而不会报错。
可以推测,在dex2oat生成AOT机器码时是有做一些检查和优化的,由于在dex2oat编译机器码时确认了两个方法同属一个类,所以机器码中就不存在权限检查的相关代码。
同包名下的权限问题
但是,并非所有方法都可以这么顺利地进行访问的。我们发现补丁中的类在访问同包名下的类时,会报出访问权限异常:
虽然com.patch.demo.BaseBug和com.patch.demo.MyClass是同一个包com.patch.demo下面的,但是由于我们替换了com.patch.demo.BaseBug.test,而这个替换了的BaseBug.test是从补丁包的Classloader加载的,与原先的base包就不是同一个Classloader了,这样就导致两个类无法被判别为同包名。具体的校验逻辑是在虚拟机代码的Class::IsInSamePackage中:
关键点在于,Class loaders must match这行注释。
知道了原因就好解决了,我们只要设置新类的Classloader为原来类就可以了。而这一步同样不需要在JNI层构造底层的结构,只需要通过反射进行设置。这样仍旧能够保证良好的兼容性。
实现代码如下:
这样就解决了同包名下的访问权限问题。
反射调用非静态方法产生的问题
当一个非静态方法被热替换后,在反射调用这个方法时,会抛出异常。
比如下面这个例子:
//BaseBug.test方法已经被热替换了。 ... ... BaseBug bb = new BaseBug(); Method testMeth = BaseBug.class.getDeclaredMethod("test"); testMeth.invoke(bb);
invoke的时候就会报:
这里面,expected receiver的BaseBug,和got到的BaseBug,虽然都叫com.patch.demo.BaseBug,但却是不同的类。
前者是被热替换的方法所属的类,由于我们把它的ArtMethod的declaring_class_替换了,因此就是新的补丁类。而后者作为被调用的实例对象bb的所属类,是原有的BaseBug。两者是不同的。
在反射invoke这个方法时,在底层会调用到InvokeMethod:
这里面会调用VerifyObjectIsClass函数做验证。
o表示Method.invoke传入的第一个参数,也就是作用的对象。
c表示ArtMethod所属的Class。
因此,只有o是c的一个实例才能够通过验证,才能继续执行后面的反射调用流程。
由此可知,这种热替换方式所替换的非静态方法,在进行反射调用时,由于VerifyObjectIsClass时旧类和新类不匹配,就会导致校验不通过,从而抛出上面那个异常。
那为什么方法是非静态才有这个问题呢?因为如果是静态方法,是在类的级别直接进行调用的,就不需要接收对象实例作为参数。所以就没有这方面的检查了。
对于这种反射调用非静态方法的问题,我们会采用另一种冷启动机制对付,本文在最后会说明如何解决。
即时生效所带来的限制
除了反射的问题,像本方案以及Andfix这样直接在运行期修改底层结构的热修复,都存在着一个限制,那就是只能支持方法的替换。而对于补丁类里面存在方法增加和减少,以及成员字段的增加和减少的情况,都是不适用的。
原因是这样的,一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个Dex的方法数的变化。方法数的变化伴随着方法索引的变化,这样在访问方法时就无法正常地索引到正确的方法了。
而如果字段发生了增加和减少,和方法变化的情况一样,所有字段的索引都会发生变化。并且更严重的问题是,如果在程序运行中间某个类突然增加了一个字段,那么对于原先已经产生的这个类的实例,它们还是原来的结构,这是无法改变的。而新方法使用到这些老的实例对象时,访问新增字段就会产生不可预期的结果。
不过新增一个完整的、原先包里面不存在的新类是可以的,这个不受限制。
总之,只有两种情况是不适用的:
- 引起原有了类中发生结构变化的修改
- 修复了的非静态方法会被反射调用,而对于其他情况,这种方式的热修复都可以任意使用。
总结
虽然有着一些使用限制,但一旦满足使用条件,这种热修复方式是十分出众的,它补丁小,加载迅速,能够实时生效无需重新启动app,并且具有着完美的设备兼容性。对于较小程度的修复再适合不过了。
本修复方案将最先在阿里Hotfix最新版本(Sophix)上应用,由手机淘宝技术团队与阿里云联合发布。
Sophix提供了一套更加完美的客户端服务端一体的热更新方案。针对小修改可以采用本文这种即时生效的热修复,并且可以结合资源修复,做到资源和代码的即时生效。
而如果触及了本文提到的热替换使用限制,对于比较大的代码改动以及被修复方法反射调用情况,Sophix也提供了另一种完整代码修复机制,不过是需要app重新冷启动,来发挥其更加完善的修复及更新功能。从而可以做到无感知的应用更新。
并且Sophix做到了图形界面一键打包、加密传输、签名校验和服务端控制发布与灰度功能,让你用最少的时间实现最强大可靠的全方位热更新。
一张表格来说明一下各个版本热修复的差别:
方案对比 | Andfix开源版本 | 阿里Hotfix 1.X | 阿里Hotfix最新版(Sophix) |
---|---|---|---|
方法替换 | 支持,除部分情况[0] | 支持,除部分情况 | 全部支持 |
方法增加减少 | 不支持 | 不支持 | 以冷启动方式支持[1] |
方法反射调用 | 只支持静态方法 | 只支持静态方法 | 以冷启动方式支持 |
即时生效 | 支持 | 支持 | 视情况支持[2] |
多DEX | 不支持 | 支持 | 支持 |
资源更新 | 不支持 | 不支持 | 支持 |
so库更新 | 不支持 | 不支持 | 支持 |
Android版本 | 支持2.3~7.0 | 支持2.3~6.0 | 全部支持包含7.0以上 |
已有机型 | 大部分支持[3] | 大部分支持 | 全部支持 |
安全机制 | 无 | 加密传输及签名校验 | 加密传输及签名校验 |
性能损耗 | 低,几乎无损耗 | 低,几乎无损耗 | 低,仅冷启动情况下有些损耗 |
生成补丁 | 繁琐,命令行操作 | 繁琐,命令行操作 | 便捷,图形化界面 |
补丁大小 | 不大,仅变动的类 | 小,仅变动的方法 | 不大,仅变动的资源和代码[4] |
服务端支持 | 无 | 支持服务端控制[5] | 支持服务端控制 |
说明:
- 部分情况指的是构造方法、参数数目大于8或者参数包括long,double,float基本类型的方法。
- 冷启动方式,指的是需要重启app在下次启动时才能生效。
- 对于Andfix及Hotfix 1.X能够支持的代码变动情况,都能做到即时生效。而对于Andfix及Hotfix 1.X不支持的代码变动情况,会走冷启动方式,此时就无法做到即时生效。
- Hotfix 1.X已经支持绝大部分主流手机,只是在X86设备以及修改了虚拟机底层结构的ROM上不支持。
- 由于支持了资源和库,如果有这些方面的更新,就会导致的补丁变大一些,这个是很正常的。并且由于只包含差异的部分,所以补丁已经是最大程度的小了。
- 提供服务端的补丁发布和停发、版本控制和灰度功能,存储开发者上传的补丁包。
: » Android 热更新热升级访问权限和即时生效问题
原创文章,作者:carmelaweatherly,如若转载,请注明出处:https://blog.ytso.com/251599.html