基本原理:加载类的时候是找element,每个element对于一个dex。我要把我修复的那个类单独放到dex插入dexlist前面,在你做类加载从前往后找优先从你的dex加载加载的就是你修复后的class.这就是
通过context拿到pathClassLoader,根据你下发的dex生成一个dexclassloader。
拿到两个的pathlist,在拿到两个pathlist的element,然后把生成的dexclassloader的element放到pathclassloader的element前面。然后把合并后的element赋值给pathclassloader的element
davlik虚拟机上会抛出unexpectDex崩溃()
业务情况:A引用了待修复的B类(下发的类)
在你app加载被引用类的时候(A引用B,也就是加载B类的时候)会做这样一个校验,如果你同时满足这三个条件就会崩溃
由于补丁类是单独的放在一个dex中所以第三个条件没法变。只能从1和2入手
应用安装的时候需要一个dexopt阶段,会对你的dex进行优化成odex后续运行加载的odex才能运行
检查静态方法,私有方法,构造函数,虚方法所调用的类是否根当前类在同一个dex中(A在调用上面方法时调用的BCDE类是否和A类在同一个dex上)
在同一个dex上,虚拟机就会对A类做一些优化并打上CLASS_ISPREVERIFIED标志
比如A引用B。并且A和B在一个dex里的时候A类会打上CLASS_ISPERVRIFYIED标志
在之后加载A类(dexopt阶段标记的类)的时候虚拟机会检查Verfiy标记的结果进行反向做verfiy的校验
当校验的时候同时满足上面三个条件的话就不通过抛出unexceptDex异常,只有校验通过才会吧类加载上来
这个方案肯定不满足第三个条件,所以只能从第一个或第二个条件下手
QZone从第二个条件入手通过插妆阻止preverify
解决思路:当上面那些特殊方法(构造函数,静态函数...)调用的是同一个dex上的类会被标志,那么我跨dex访问就不会打上标志。最简单的就是在构造函数里面进行访问跨dex即可,这样不在同一个dex就不会打标志
实现:
创建一个空的类放到一个独立的dex上
在所有类的构造函数里面都去访问那个独立dex里面空的类,所有的类都存在一个跨dex的访问,所以整个app里面的所有类都不会被打伤标志
但是独立的dex需要先被加载进来,因为APP的PathClassLoader找不到这个类。利用双亲委派模型机制(加载类的时候先从缓冲中找)先把这个空类加载进来后续就可以访问到这个类了。
缺点:
影响了odex的校验和优化过程存在一个性能的问题
降低APP启动性能,运行内存增加
从抛出的第一个条件入手
针对静态类调用和instanceof这两种方式以外的方式会抛异常
如果我以静态类来调用补丁类的话即使存在跨dex调用被打伤标志也不会抛出异常,同时classloader加载类的时候只要加载过会优先从缓存里面读利用这个机制。
davlik虚拟机加载类的过程:
先会从dex的缓存里面找如果有就直接返回不会有后续的校验和加载过程,后面加载和校验完成后也会放到dex的缓存里面
APP启动的时候把补丁类放进来以后,提前以静态方式引用补丁类,这个引用不会抛异常(静态类引用方式)同时会让这个补丁类提前加载到虚拟机的缓存中,后面的访问即使是非静态的即使有标志冲突的也不需要进行校验了。可以直接返回后续从缓冲中读到这个类
因为调用的是虚拟机的native方法加载类,所以在不同虚拟机上有较多的适配,同时会有稳定性的问题。分享文档里面说出来在在X86上有问题
不仅仅是下面级联优化的问题,还有其他问题在dex流派上在标注
Art虚拟机上由于方法内联会带来更大的问题,不管是哪个虚拟机在安装阶都有个dex优化的过程
不同安卓版本有不同的odex编译器,早期编译器用的是QuickCompile后面用的比较多的是OptimizingCompare
不同的编译器进行方法内联时有不同的方法条件,并且Optiminzing有级联优化操作(method1调用method2里面调用method3里面调用method4)如果这些调用的方法都满足虚拟机的内联条件。
最终编译后的method1里面直接包含了method2method3method4的代码(方法
2包含3和4的代码,3包含4的代码),内联的意思是把代码直接写进来而不是通过方法id进行调用
假如ClassA正好要引用你的补丁类,而补丁类之前在虚拟机优化的时候满足内联条件,那么老的方法已经被写到引用类里面了。这时候在下发新的class修复的时候可以正常加载class,但是方法的调用并没有调用到你的新类class上来,因为你的实现已经被写到引用类里面了。就会存在问题
由于内联,执行流程并未跳转到新的方法里面,引用类里面的方法是用的老的方法。对于引用类来说用的还是老方法中局部变量表存放的内容 所以查找成员字符串都是用的旧方法的索引。但是新的补丁类索引是可能发生变化的引用类访问的时候就会出现crash出错的问题。
由于级联优化的存在因此把你要修复的类,你的子类,调用你的类都必须整个放到patch里面,下发整个patch,所以整个patch会很大
class是干扰的系统api较为底层所以存在适配和兼容性问题。
后来tinker走上了dex存量热修复的路径
原理:进行全量dex的替换,但是不可能吧整个dex下发,所以下发的是dex的diff。
新老dex的diff在服务端生成,通过diff算法:
Sigma用的是比较常见的BsDiff
tinker做的比较深入依据dex结构发明了一个dexdiff算法,让你diff差异包更小,合成效率更高
parch进程中PathCore合并核心代码中的一些操作是和Application一起由PathClassLoader加载的,如果你的pathcore调用了你的业务逻辑没有做解耦的话, 那么这个时候path会加载你的旧业务的类(由pathclassloader加载),由于双亲委派模型后续这些旧业务的类是从pathClassLoader缓存拿的而不是从你patch进程做完合并后的dexclassloader拿的就会出现问题导致调用类和加载类不一致,所以需要进行和业务解耦。
就是如果在生成新的dex替换pathclassloader的parent之前访问了之前的类,那么是由pathClassLoader加载的,就会导致加载的类是旧的dex。而因为有缓存,一直是拿的pathClassLoader加载的类而不是合并后修复完成的dexClassLoader的类
dex的热修复有一些基本典型的问题需要解决:
直接手动new一个dexclassloader,然后虚拟机就会做全量的dexopt在独立进程中(虽然dexopt过程放到了独立的patch进程做,但是还是会存在部分anr,后面问题在列出)
dex2oat是对dex进行编译的一个进程。在art虚拟机上你的dex是需要编译成机器码以后才能被虚拟机加载和运行的
编译过程有十几种模式,比较关心的只有三种:
全量编译机器码:art虚拟机为了提高性能,会对代码做全量机器码编译。这个过程会在ClassLoader加载类的时候发现传入进来的opt路径上不存在odex文件的时候就会自动触发。因为是第一次newclassloader之前没有做过编译也就没有odex文件所以就会做全量编译
轻量编译也有一定耗时,导致首次启动慢。而且你轻量编译之后你的独立进程也是无限制的在做全量编译可能抢资源导致主进程拖满然后ANR(概率较小tinker准备忽略,因为APP性能足够好)
有三种方案:
混合编译:AOT,解释,JIT三种模式并存。
用户真正使用到的类可能只有很少部分,我们为什么要为了百分之二三十的代码去做全量编译呢?没有必要
N之前的Art虚拟机上安装是做的全量编译,所以安装的时候会等很久,做Jit及时编译又会很慢
在N上解决了这个问题通过混合编译缩短安装时间,系统OAT升级更快: 安装和首次启动用intervept-only的方式没有编译(和davlik虚拟机一样的效果),对哪些代码做编译,什么时候做编译呢:来看N上的增量编译过程:
虚拟机会在APP代码运行过程中收集运行到的代码放到profile文件上,系统会通过jobSchedule启动BackgroundDexOptService。这个Service会在灭屏/充电的状态下启动。晚上睡觉或者其他手机空闲的情况时就会启动任务把收集到的代码给编译好(这些热代码是经常跑的所以会快)。后面启动的时候就会很块,通过这种方式给APP做增量的编译。编译完之后会生成base.odex和base。art(称之为App的image)
虚拟机认为这是热代码所以在你APP启动的时候就提前帮你吧这部分代码加载起来。在ClassLoader创建ClassLinker的时候一次性加载到dexcache上
所以就是你刚启动Application里面什么都还没做就已经加载了一些类(以前编译好的热代码)
在N以上的设备抛弃设置parent的模式,做全量的直接替换吊我们的pathclassloader而不是设置他的parent
原理:因为系统的appimage提前加载是加载到系统的pathClassloader缓存上的。而我们后续运行的是用我们替换的classloader,所以这个新的classloader上没有了appimage的存在了
影响:由于没有了appimage的存在所以性能上会有牺牲但是是能达到修复的目的,统计下来影响是非常小的
本文为转载文章
原文链接:热修复Class流派和Dex流派实现原理 - 掘金 (juejin.cn)