热修复技术学习总结

来源:互联网 发布:截图识别文字软件 编辑:程序博客网 时间:2024/05/02 13:48

注明:

本文章是对阿里开放出的《深入探索Android热修复技术原理7.3Q.pdf》进行阅读后的总结性文章,只包含个人认为有用的内容,目的是方便以后回忆或者分析问题,想深入学习可自行研究。附上原书http://pan.baidu.com/s/1dE7i8NJ

三大修复原理简要

1.代码修复

1.1 即时生效:底层替代类中的老代码,并且无视底层的具体结构。
1.2 重启生效:基于类加载机制,重新编排了包中dex的顺序。

2.资源修复

2.1 传统的资源修复是基于InstantRun的原理,就是构造一个新的AssetManager,将新的资源进行addAssetPath,然后通过反射替换掉系统中的原理的AssetManager的引用。
2.2 阿里采用的是直接将一个比系统资源包的packageId 0x7F小的packageId为0x66的资源addAssetPath到原来的AssetManager对象上即可,这个补丁资源包只包含新添加,和已修改的。

3.so修复

本质是对native方法的修复和替换,阿里采用的是类似类修复反射注入的方式,把补丁so路径插入到nativeLibrary数组的最前面。

代码热修复

  1. 底层热替换原理

1.1 Andfix 原理:通过jni的replaceMethod(Method src ,Method des )->通过 env的FromReflectMethod得到ArtMethod地址,转为ArtMethod指针->挨个替换ArtMethod的中字段.

1.2 虚拟机调用方法的原理 : 最终ArtMethod中的字段(例如entry_point_from_interpreter)找到最终要执行的方法的入口地址,art可以采用解释模式或者AOT机器码模式执行。

1.3 Andfix原理兼容性的根源 : ArtMethod的结构厂商可以自己改变,就会导致替换字段信息不是代码中指定的信息,导致替换错乱

1.4 突破底层ArtMethod结构的差异 : 将ArtMethod整体替换,阿里的核心方法是memcry(smeth,dmeth,sizeOf(ArtMethod))。这里面的关键是sizeOf(ArtMethod)的实现,其原理是ArtMethod的存储接口是线性的,通过两个ArtMethod的地址差就可以。这种方式的适配不受系统的影响,稳定且兼容。

1.5 访问权限的问题 :
* 方法时访问权限 : 机器码中不存在检查权限的相关代码
* 同包名下访问权限的问题 : 由于补丁包的ClassLoader与原来的ClassLoader不一致,导致虚拟机代码的Class::IsInSamePackage校验失败。解决方案就是通过反射让补丁包的ClassLoader为系统原来的ClassLoader即可。
* 被反射调用的方法问题 : 由于ArtMethod中的declaring_class_被替换成了新的类,而反射得到的还是原来的老类,这会导致invoke时VerifyObjectClass()方法失败,而直接报错。所以这种热修复方案不能修复这种方法。

1.6 即时生效的限制:
引起类中发生结构变化的修改 : 因为一旦引起修改ArtMethod的位置将发生变化,就找不到地址了。
修复了的非静态方法被反射调用。

2 java中的秘密

2.1 内部编译类
* 内部类在编译器会被编译为跟外部类一样的类
* 静态内部类与非静态内部类,在smali中非静态内部类会自动合成this$0 域标示的是外部类的引用。
* 外部类为了访问内部类(或内部类访问外部类)的私有域,编译期间会自动为内部类(或外部类)生成access&XXX方法。
* 热修复替换时,要避免生成access&XXX方法,就要求内/外部类不能存在private的method/field。

2.2 匿名内部类
* 匿名内部类的名称格式一般为外部类&number,number根据匿名内部类出现的顺序累加记名。
* 如果在之前增加一个匿名内部类 则会导致原来的匿名内部类名称不对应。也就无法使用热修复。
* 应当极力避免插入新匿名内部类,特别是向前插。

2.3 域编译
* 热替换不支持 clint方法
* 静态域和静态代码块在clint方法中
* 非静态在init方法中
* 静态域和静态代码块不支持热替换

2.4 final static 域
* final static 原始类型和字符串在initSField而不是在clint中
* final static 引用类型在 clint方法中初始化
* 优化时final static 对于原始类型和字符串有用,引用类型其实没有用。

2.5 方法编译
* 混淆可能导致方法的内联和裁剪
* 被内联:方法没被用过,方法只有一行代码,方法只被一个地方引用过。
* 被裁剪:方法中有参数没被使用。
* 热替换解决方法:在混淆是加上配置 -dontoptimize

2.6 switch case 语句编译
* 连续几个相近的值会被编译为packed-switch指令,中间差值用pswitch-0补齐。
* 不连续边被编译为sparse-switch指令
* 热替换方案:资源id为const final static 会被编译为packed-switch指令,会存在资源id替换不完全的问题,解决方案就是修改smali反编译流程,碰到packed-switch指令强替换为sparse-switch指令,:pswitch-N标签强改为sswitch-N标签,然后做资源id的强替换,在回编译smali为dex。

2.7 泛型编译
* 泛型在编译器中实现,虚拟机无感知
* 泛型类型擦除:编译器在编译期间将泛型转为目标类型的字节码,对于虚拟机来说得到的是目标类型的字节码文件,无感知泛型。
* 泛型与多态冲突的原理及方案:

*类型擦除后 原来的set(T t)的字节码会是set(Object t) 而其子类为set(Number t),从重写的定义上来看这不是重写而是重载。这也就导致泛型和多态有冲突了
*而实际是可以重写的,其本质原因是JVM采用了bridge方法。子类真正重写父类方法是bridge方法,而在bridge方法中调用了子类的方法而已。@override只是个假象。

*泛型不需要强制类型转换的原因是:编译器如果返现有一个变量申明加上了泛型的话,编译器会自动加上chceck-cast类型转换。

2.8 Lambda 表达
* Lambda 会被;;其内部this指的是外部类对象,这点区别于内部类的this。
* 函数式接口 : 只有一个方法的接口
* 函数式接口调用时,最终会增加一个辅助方法。不能走热替换
* 修改函数式接口内部逻辑可以走热替换

2.9 访问权限检查对热替换的影响
*补丁类如果引用了非public类,最终会抛dvmThrowException

2.10 Clint方法
* 不支持clint方法的热替换

3 冷启动方案

3.1 传统实现方式的利弊
* QQ控件的插庄方案:

原理:单独放一个类在dex中,让其它类调用,防止打上CLASS_ISPREVERIFIED标志,再加载补丁dex得到dexFile对象作为参数构建一个Element对象插入到dex-Elements数组的前面。
缺点: Dalvik下影响类加载性能,Art下类地址写死,导致必须包含父类或引用,最后导致补丁包很大

*Tinker方案:

原理:提供dex差量包,整体替换dex的方案。差量的方式给出patch.dexm,然后将patch.dex和应用的classes.dex合并成一个完整的dex,完整的dex加载得到的dexFile对象作为参数构建一个Elements对象然后整体替换掉旧的dex-Elements数组。
缺点: dex合并内存消耗在Vm heap上,容易OOM,最后导致dex合并失败

3.2 插桩实现的前因后果
默认一个dex时所有类会打上CLASS_ISPREVERIFIED标志,新的补丁类不在原dex中时,被调用会报dvmThrowllegalAccessError。一个单独的辅助类放到一个单独的dex中,原dex的所有类的构造函数都引用这个类,dexopt时原Dex所有类不会被打上CLASS_ISPREVERIFIED这个标志。

3.3 插桩导致类加载性能影响
采用插桩,导致所有类都是非preverify,这就使得dexopt和load class时频繁的verify和optimize。当类很多时这个操作会相当耗时,导致启动时长时间白屏。

3.4 避免插桩的QFix方案
在dexopt后进行检查绕过,会存在潜在的Bug

3.5 Art下冷启动实现
将补丁直接命名为classes.dex 将原来的一次命名为classes1.dex …classes2.dex…等。然后一起打包为一个apk。然后DexFile.loadDex得到DexFile对象,最后把该DexFile对象整体替换旧的dexElements数组

未完待续

原创粉丝点击