Android 热更新热升级访问权限和即时生效问题

前段时间 IOS 取消了相关框架的热更,在 CSDN 社区专家群引起了大量的讨论。大家纷纷发表自己的看法和技术解决方案。本文不是 IOS 相关的问题的解决方案,主要围绕 Android 的热更新进行学习!

综合前面的几篇文章《Android热修复升级原理和实践》、《Android热修复升级、兼容性问题的根源》、《详解 Android 热更新升级如何突破底层结构差异?》。

看了前面的几篇文章,你可能会有疑惑:我们只是替换了ArtMethod的内容,但新替换的方法的所属类,和原先方法的所属类,是不同的类,被替换的方法有权限访问这个类的其他private方法吗?

以这段简单的代码为例

public class Demo{
	Demo{
		func();
	}
	private void func(){}
}

Demo构造函数调用私有函数func所对应的Dex Code和Native Code为

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中:

Classloader

关键点在于,Class loaders must match这行注释。

知道了原因就好解决了,我们只要设置新类的Classloader为原来类就可以了。而这一步同样不需要在JNI层构造底层的结构,只需要通过反射进行设置。这样仍旧能够保证良好的兼容性。

实现代码如下:

Classloader

这样就解决了同包名下的访问权限问题。

反射调用非静态方法产生的问题

当一个非静态方法被热替换后,在反射调用这个方法时,会抛出异常。

比如下面这个例子:

//BaseBug.test方法已经被热替换了。
... ...
BaseBug bb = new BaseBug();
Method testMeth = BaseBug.class.getDeclaredMethod("test");
testMeth.invoke(bb);

invoke的时候就会报:

Caused By:java.lang.IllegalArgumentException:

这里面,expected receiver的BaseBug,和got到的BaseBug,虽然都叫com.patch.demo.BaseBug,但却是不同的类。

前者是被热替换的方法所属的类,由于我们把它的ArtMethod的declaring_class_替换了,因此就是新的补丁类。而后者作为被调用的实例对象bb的所属类,是原有的BaseBug。两者是不同的。

在反射invoke这个方法时,在底层会调用到InvokeMethod:

InvokeMethod

这里面会调用VerifyObjectIsClass函数做验证。

Method.invoke

o表示Method.invoke传入的第一个参数,也就是作用的对象。
c表示ArtMethod所属的Class。

因此,只有o是c的一个实例才能够通过验证,才能继续执行后面的反射调用流程。

由此可知,这种热替换方式所替换的非静态方法,在进行反射调用时,由于VerifyObjectIsClass时旧类和新类不匹配,就会导致校验不通过,从而抛出上面那个异常。

那为什么方法是非静态才有这个问题呢?因为如果是静态方法,是在类的级别直接进行调用的,就不需要接收对象实例作为参数。所以就没有这方面的检查了。

对于这种反射调用非静态方法的问题,我们会采用另一种冷启动机制对付,本文在最后会说明如何解决。

即时生效所带来的限制

除了反射的问题,像本方案以及Andfix这样直接在运行期修改底层结构的热修复,都存在着一个限制,那就是只能支持方法的替换。而对于补丁类里面存在方法增加和减少,以及成员字段的增加和减少的情况,都是不适用的。

原因是这样的,一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个Dex的方法数的变化。方法数的变化伴随着方法索引的变化,这样在访问方法时就无法正常地索引到正确的方法了。

而如果字段发生了增加和减少,和方法变化的情况一样,所有字段的索引都会发生变化。并且更严重的问题是,如果在程序运行中间某个类突然增加了一个字段,那么对于原先已经产生的这个类的实例,它们还是原来的结构,这是无法改变的。而新方法使用到这些老的实例对象时,访问新增字段就会产生不可预期的结果。

不过新增一个完整的、原先包里面不存在的新类是可以的,这个不受限制。

总之,只有两种情况是不适用的:

  1. 引起原有了类中发生结构变化的修改
  2. 修复了的非静态方法会被反射调用,而对于其他情况,这种方式的热修复都可以任意使用。

总结

虽然有着一些使用限制,但一旦满足使用条件,这种热修复方式是十分出众的,它补丁小,加载迅速,能够实时生效无需重新启动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] 支持服务端控制

说明:

  1. 部分情况指的是构造方法、参数数目大于8或者参数包括long,double,float基本类型的方法。
  2. 冷启动方式,指的是需要重启app在下次启动时才能生效。
  3. 对于Andfix及Hotfix 1.X能够支持的代码变动情况,都能做到即时生效。而对于Andfix及Hotfix 1.X不支持的代码变动情况,会走冷启动方式,此时就无法做到即时生效。
  4. Hotfix 1.X已经支持绝大部分主流手机,只是在X86设备以及修改了虚拟机底层结构的ROM上不支持。
  5. 由于支持了资源和库,如果有这些方面的更新,就会导致的补丁变大一些,这个是很正常的。并且由于只包含差异的部分,所以补丁已经是最大程度的小了。
  6. 提供服务端的补丁发布和停发、版本控制和灰度功能,存储开发者上传的补丁包。

Android 热更新热升级访问权限和即时生效问题

: » Android 热更新热升级访问权限和即时生效问题

原创文章,作者:carmelaweatherly,如若转载,请注明出处:https://blog.ytso.com/251599.html

(0)
上一篇 2022年5月3日
下一篇 2022年5月3日

相关推荐

发表回复

登录后才能评论