热更新总结--热部署更新

本文章主要根据阿里出的《深入探索Android热修复技术原理》后的个人总结

 

打补丁是通过反编译为smali然后新APK跟基线APK进行差异对比,得到最后的补丁包。

类替换(关键点ArtMethod)

replaceMethod(src,dest)

artMethod ptrsizedfields

入口

Method dispatch from quick compiled code invokes this pointer which may cause bridging into the compiled code

entry_point_from_interpreter_

入口

Method dispatch from quick compiled code invokes this pointer which may cause bridging into the interperter

方法从快速编译的代码调用这个指针,这可能会导致桥接到中介器中。

entry_point_from_quick_compiled_code_

 

Art虚拟机解释模式或者AOT机器模式执行DEX CODE。

 

解释模式,就是取出Dex Code然后逐条执行,这就会调用entry_point_from_interpreter_

AOT机器码模式,就是先把Dex Code取出转化成机器码,然后再直接运行机器码,不需要逐条解析DEX CODE,这就会去entry_point_from_quick_compiled_code_执行

 

Art虚拟机在加载初始化一个类的时候,会给这个类分配内存地址,class_linker这个类中会有两个方法,一个是direct_methods,一个是virtual_method,前者是包含static方法和所有的不可继承的对象方法,后者是包含所有可以继承的对象方法。

 

AllocArtMethodArray创建了一个method数组,这个数组的指针是ptr,方法是连续排列创建出来放在数组中的。

 

HotFix(阿里)将AndFix(Google)的方案,字段替换转化为方法对象替换,其中难点是对象Size的确定,因为ArtMethod的大小不确认,最后通过生成两个静态方法,然后地址相减得到具体值,这个方案的实现基底是因为MethodArray生成的时候,会将方法连续排列在一起。

 

memcpy

memcpy指的是c和c++使用的内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。

void *memcpy(void *dest, const void *src, size_t n);

 


疑问,

权限访问的问题,私有类只是替换了方法,这个方法真的可以访问这个类中相关的私有函数或对象吗?

在执行方法时,即便替换了func也没有任何的权限校验方法,因此无所谓替换不替换都可以直接跳转到方法内执行。


Class::IsInSamePackage会判断是否在同一个包下,如果是不同的ClassLoader加载的class就算包名相同也不会被判断为同一个包下,因此就算替换了方法,也无法调用同包下的类

 

 因此想替换方法,就需要先拿到之前类的ClassLoader,这个通过Java反射就可以。

 f.setAccessible(true);得作用就是让我们在用反射时访问私有变量

 

Field

 

Field classLoadField = Class.class.getDeclaredField("classLoader");

classLoadField.setAccessible(true);

classLoadField.set(newClass, oldClass.getClassLoader);

 

 


无法绕过的问题,

1、如果方法中有反射调用非静态类,此时在反射中会有VerifyObjectIsClass(receiver, declaring_class)

校验,内部会判断Object是否在Class中,

if(UNLIKELY(!o->InstanceOf(c)))

o:Method.invoke传入的第一个参数,也就是作用的对象

c:ArtMethod所属的类型

 

2、只能支持方法替换,无法支持增加或者减少,以及成员字段的增加或减少都无法使用。但是可以新增一个原代码中没有的新类是可以的。


内部类为什么有问题,

因为在调用时,原来没有内部类的时候没有生成外部类或者内部类没有生成access方法,此时内部类和外部类只有一个access方法,因此热更新失败。

 

内部类,无论是否静态都是顶级类,跟普通类编译后是相同的,因此两个类想相互调用私有函数或者对象的时候,内部类和外部类就会生成对应的access方法。

 

匿名名内部类会避免了调用外部类的私有方法或者后,仍然会有问题,因为内部类生成的方法会根据“外部类$数字”来命名,原来有一个,现在有多个,调用的方法的顺序就会变错,数量也会产生问题,因此就都乱套了。

除非你的匿名内部类插入到最后。因为数字是顺序的。


对于静态引用类型,

由于在Clint里面初始化,Clinit又是在类加载的时候就已经调用,因此无法热更新成功。

 

但是final static的基础类型和非引用的String类型,在编译期间引用基础类型的地方被立即数替换,应用到String类型的地方被常量池索引ID替换,所以在热部署模式下,最终所有引用到该final static域的地方都会被替换。

 

对于final static引用类型域是不允许的,因为他们在clinit里面初始化。


混淆时会有方法的内联和方法的裁剪,

此时可以添加-dontoptimize就可以取消方法内联和裁剪

 

Android虚拟机运行的是dex文件,因此在混淆的时候预编译是没有任何意义的,反而会增加混淆速度,因此一般需要加上-dontpreverify

 


Lambda表达式 热更新

 

Java7 JVM中新增加了一个指令 invokedynamic,用于支持动态语言。lambda表达式就是动态语音,为了支持它新增了invokedynamic。

函数式接口:它是一个接口,这个接口具有唯一一个抽象方法,我们将同时满足这两个条件的接口成为函数式接口。

lambda表达式是Java现在提供的最接近闭包的概念。

 

Android虚拟机首先通过javac把源码编译成.class然后再通过dx工具优化成适合移动设备的dex字节码文件。

但是如果在Android中使用新的Java8语言特性,还需要使用新的Jack工具链来替换掉旧的工具链来编译。Jack拥有自己的.jack库格式,Jack是Java Android Compiler Kit的缩写,它可以将Java代码编译成Dalvik字节码,并负责Minification、Obfuscation、Repackaging、Multidexing、Incremental Compilation。Jack试图取代javac,dx,proguard,jarjar,multidex库等工具。

 

旧版javac工具链:

javac(.java->.class) -> dex(.class->.dex)

Android 新版Jack工具链:

Jack(.java->.jack->.dex)

 

 .dex字节码和.class字节码对Lambda表达式处理的异同点:

相同点:

编译期间都会生成一个static辅助方法,该方法内部逻辑实现Lambda表达式。

不同点:

1、.class字节码中通过invoke-dynamic指令执行Lambda表达式,而dex字节码中直接Lambda表达式跟普通方法调用没有任何区别。

2、.class字节码中运行时生成新类,而.dex字节码中编译期间生成新类。

 

总结:

1、因此也就是说新增或者减少一个Lambda表达式会造成类的内部方法减少或者增加,这个内部方法指的就是代理类。这样热部署就会失败!

2、如果只是修改了Lambda表达式内部,但是由于是内部类,之前说的非静态内部类的时候就很明确的规定,如果非静态内部类需要访问外部类的非静态字段或者方法时,就要生成一个外部的代理,也就是说需要持有外部类的引用。假设内部类之前没有访问外部非静态字段或者方法,新内部类有的话,就会造成热部署失败!即:

单纯修改一个Lambda表达式,可能导致新增Field,所以此时也会造成热部署失败。


访问权限,类加载时父类和接口的访问权限,类校验阶段访问权限,

 

父类和接口的访问权限:

主要判断的是classloader是否相同,如果不同在编译时就会直接报错。

 

类校验阶段访问权限:

会进行 dvmVerifyClass校验类—>verifyMethod校验方法—>(dvmVerifyCodeFlow—>doCodeVerification)对每个方法的逻辑进行校验—>verifyInstruction对每个指令进行校验。

补丁类如果引用了非public类,那么verifyInstruction方法执行结果为pFailure = VERIFY_ERROR_ACCESS_METHOD,而对于VERIFY_ERROR_ACCESS_METHOD的处理就是替换潜在错误的指令码,也就是把权限校验失败的指令码例如new-instance等替换成OP_THROW_VERIFICATION_ERROR指令。此时verifyInstruction仍然返回的是true,所以补丁加载时是成功的,但是执行时会抛出异常,中断程序!

 

也就是说前者在编译时可以发现是错误,然后编译失败之后通过classloader替换等进行替换,实现热部署,但是后者只有在运行时才会报错直接退出~

 

 


总结:

 

由于补丁热部署方案的特殊性----不允许类结构变更以及不允许变更<clinit>方法,所以补丁工具发现这几种情况时只能走冷启动重启生效方案,冷启动几乎是无限制的。

 

热替换方式的修复无须重启可以直接生效,这种立竿见影的修复效果也是它最大的优势。然而除了以上几种几种情况限制不能使用热部署外,还有就是在程序运行时发生了变动,如果改了其中的方法逻辑,可能会造成前后逻辑不一致,发生一些诡异的错误。因此热部署最好只用于一些简单BUG的修复,如果做一些功能上的更新不建议使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值