Class门户原理

根本原理:加载类的时分是找element,每个element关于一个dex。我要把我修正的那个类独自放到dex刺进dexlist前面,在你做类加载早年往后找优先从你的dex加载加载的便是你修正后的class.这便是

完结代码

  1. 经过context拿到pathClassLoader,根据你下发的dex生成一个dexclassloader。

  2. 拿到两个的pathlist,在拿到两个pathlist的element,然后把生成的dexclassloader的element放到pathclassloader的element前面。然后把兼并后的element赋值给pathclassloader的element

Davlik虚拟机上遇到的问题

unexpectDex溃散

davlik虚拟机上会抛出unexpectDex溃散()

事务状况:A引证了待修正的B类(下发的类)

抛出unexpectDex溃散一同满意的三个条件

抛出这个溃散需求一同满意三个条件:

热修复Class流派和Dex流派实现原理

  1. 补丁类不是经过静态类或许instance of的办法被引证
  2. 引证下发补丁的类在dexopt阶段verify成功,引证类被打上了CLASS_ISPERVRIFYIED的标志
  3. 这两个类不在一个dex上

在你app加载被引证类的时分(A引证B,也便是加载B类的时分)会做这样一个校验,假设你一同满意这三个条件就会溃散

因为补丁类是独自的放在一个dex中所以第三个条件没法变。只能从1和2入手

使用装置的时分需求一个dexopt阶段,会对你的dex进行优化成odex后续运转加载的odex才干运转

dexopt阶段的进程

检查静态办法,私有办法,构造函数,虚办法所调用的类是否根当时类在同一个dex中(A在调用上面办法时调用的BCDE类是否和A类在同一个dex上)

在同一个dex上,虚拟机就会对A类做一些优化并打上CLASS_ISPREVERIFIED标志

比方A引证B。并且A和B在一个dex里的时分A类会打上CLASS_ISPERVRIFYIED标志

何时抛出反常

在之后加载A类(dexopt阶段符号的类)的时分虚拟机遇检查Verfiy符号的成果进行反向做verfiy的校验

当校验的时分一同满意上面三个条件的话就不经过抛出unexceptDex反常,只要校验经过才会吧类加载上来

QZone插桩安排preverify计划

这个计划必定不满意第三个条件,所以只能从第一个或第二个条件下手

QZone从第二个条件入手经过插妆阻挠preverify

处理思路:当上面那些特别办法(构造函数,静态函数…)调用的是同一个dex上的类会被标志,那么我跨dex拜访就不会打上标志。最简单的便是在构造函数里边进行拜访跨dex即可,这样不在同一个dex就不会打标志

完结:

创立一个空的类放到一个独立的dex上

在一切类的构造函数里边都去拜访那个独立dex里边空的类,一切的类都存在一个跨dex的拜访,所以整个app里边的一切类都不会被打伤标志

可是独立的dex需求先被加载进来,因为APP的PathClassLoader找不到这个类。使用双亲派遣模型机制(加载类的时分先从缓冲中找)先把这个空类加载进来后续就能够拜访到这个类了。


缺点:

影响了odex的校验和优化进程存在一个功能的问题

下降APP发动功能,运转内存增加

Qfix提早constclass引证计划

从抛出的第一个条件入手

针对静态类调用和instanceof这两种办法以外的办法会抛反常

假设我以静态类来调用补丁类的话即使存在跨dex调用被打伤标志也不会抛出反常,一同classloader加载类的时分只要加载过会优先从缓存里边读使用这个机制。

davlik虚拟机加载类的进程:

先会从dex的缓存里边找假设有就直接返回不会有后续的校验和加载进程,后边加载和校验完结后也会放到dex的缓存里边

完结思路

APP发动的时分把补丁类放进来今后,提早以静态办法引证补丁类,这个引证不会抛反常(静态类引证办法)一同会让这个补丁类提早加载到虚拟机的缓存中,后边的拜访即使是非静态的即使有标志抵触的也不需求进行校验了。能够直接返回后续从缓冲中读到这个类

完结代码:

  1. 在application最开始的当地用静态类办法加载补丁类,可是咱们并不知道要修正哪个类,所以不可能在application里边把一切的类都加载一遍(哈哈哈,不科学)
  2. QFix经过nativehook直接调用虚拟机加载类的native办法,APP打包的时分保存了各个类的dexId和classId。在运转的时分找到补丁类所在的dexid和classid在jni侧主动调用虚拟机解析class的办法(设置formUnverifedConstant参数为true代表这次的调用是以constantof或许是以instanceof的办法调用进来,这个为true就不会做preverify的校验),这一次调用你的补丁就在缓存里边了,后续使用就直接从缓存里边找就能够,也不需求进行校验了

因为调用的是虚拟机的native办法加载类,所以在不同虚拟机上有较多的适配,一同会有稳定性的问题。共享文档里边说出来在在X86上有问题

Art虚拟机上遇到的问题

不仅仅是下面级联优化的问题,还有其他问题在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会很大

Dex门户热修正原理

class是搅扰的体系api较为底层所以存在适配和兼容性问题。

后来tinker走上了dex存量热修正的途径

原理:进行全量dex的替换,可是不可能吧整个dex下发,所以下发的是dex的diff。

新老dex的diff在服务端生成,经过diff算法

Sigma用的是比较常见的BsDiff

tinker做的比较深入根据dex结构发明晰一个dexdiff算法,让你diff差异包更小,合成效率更高

过程

  1. 服务端生成了新老dex的diff之后就会生成差异包。差异包会被你的patch进程恳求到和本地根据装置的dex进行merge成新的dex也便是经过patch还原成新的dex
  2. 经过新dex创立出一个新的dexclassloader,把这个新dexclassloader设置成App的pathclassloader的parent。根据双亲派遣模型你加载的便是新的dexclassloader,也便是修正后的类

修正为什么要在独立进程做?

  1. 即使事务进程无线溃散,patch进程也能修正你的问题
  2. 事务进程可能在做迭代,做兼并可能会呈现crash
  3. 独立进程中做的话不依赖于主进程发动,其他事务进程的发动也能够吧patch进程拉起来进行统一的修正

注意点

parch进程中PathCore兼并中心代码中的一些操作是和Application一同由PathClassLoader加载的,假设你的pathcore调用了你的事务逻辑没有做解耦的话, 那么这个时分path会加载你的旧事务的类(由pathclassloader加载),因为双亲派遣模型后续这些旧事务的类是从pathClassLoader缓存拿的而不是从你patch进程做完兼并后的dexclassloader拿的就会呈现问题导致调用类和加载类不共同,所以需求进行和事务解耦。

便是假设在生成新的dex替换pathclassloader的parent之前拜访了之前的类,那么是由pathClassLoader加载的,就会导致加载的类是旧的dex。而因为有缓存,一向是拿的pathClassLoader加载的类而不是兼并后修正完结的dexClassLoader的类

根本的共性问题

dex的热修正有一些根本典型的问题需求处理:

  1. patch的入口和patch的中心事务需求和事务进行阻隔
  2. patch兼并需求放到独立进程做
  3. 每次打包的mapping会改变:假设不对混杂进行干涉,每次打包的混杂规则是会改变的,所以会导致哪怕是很小的改动也会导致两个包的dex差异十分大所以需求对混杂的mapping进行保存,在打新包的时分apply这个mapping就会保持混杂共同不会导致差异
  4. 每次打包的分包成果会改变:假设APP大的话,会存在跨dex拜访(针对这种多dex状况哪怕你没有修正也会导致他的分包成果不相同)。所以在打基准包的时分也要把他的分包成果保存下来(打新包时按照这个成果进行分包)
  5. patch进程做完patch兼并之后,主进程使用patch的时分会立马黑屏或许anr。虚拟机是不会直接拜访dex的有个dexopt阶段(使用装置时分做的,动态加载dex时也会做这个阶段)。dexopt是由体系触发的。所以会黑屏便是因为你的主进程直接用得动态加载的dex触发了dexopt导致黑屏。所以在patch进程兼并完新的dex之后应该立刻去触发dexopt.

如何触发dexopt

直接手动new一个dexclassloader,然后虚拟机就会做全量的dexopt在独立进程中(尽管dexopt进程放到了独立的patch进程做,可是仍是会存在部分anr,后边问题在列出)

Art dex2oat对热修正影响

dex2oat是对dex进行编译的一个进程。在art虚拟机上你的dex是需求编译成机器码今后才干被虚拟机加载和运转的

dex2oat编译形式

编译进程有十几种形式,比较关心的只要三种:

  1. interpret only:该形式在first boot或许install的时分(第一次发动或许装置)进行。只会做verify,代码仍是解释履行,不做机器码的编译操作。功能是和davlik虚拟机保持共同
  2. speed:该形式在new DexClassLoader的时分触发。会做全量的机器码编译
  3. speed profile:该形式在体系做oat升级的时分或许混合编译(有一个background的dexopt在体系idle的时分会唤醒做dexopt)的时分,他只编译你app对应的profile里保存的热代码,只编译这部分热代码。

全量编译机器码:art虚拟机为了提高功能,会对代码做全量机器码编译。这个进程会在ClassLoader加载类的时分发现传入进来的opt途径上不存在odex文件的时分就会主动触发。因为是第一次newclassloader之前没有做过编译也就没有odex文件所以就会做全量编译

处理计划演进

  • 所以假设主进程发动直接做全量编译直接挂
  • 假设在patch进行全量编译,因为dex2oat进程十分长在部分机型到达几分钟好的机型上也得等二三十秒并且十分占资源就有可能你的整个apt进程没做完来不及。比方用户总是点开新闻看几秒就杀掉导致你一向做不完优化,修正就一向用不上可能会拖慢主进程导致ARN
  • tinker的计划:所以patch进程先进行轻量编译,假设做完了就用,做不完的话使用老的先让用户能用,并且防止全量编译(你都用的是老的了,就没必要做过多的全量编译了可能会导致占用资源过多事务进程也卡)。假设patch做轻量量编译能够用就用,不能用防止全量编译先让用户跑起来(如何防止全量编译稍后介绍)

轻量编译也有必定耗时,导致首次发动慢。并且你轻量编译之后你的独立进程也是无限制的在做全量编译可能抢资源导致主进程拖满然后ANR(概率较小tinker预备疏忽,因为APP功能足够好)

  • App在前台运转也有可能会导致patch进程抢占资源导致anr。所以在这个基础上面进一步优化:patch进程拉倒patch后先进行轻量编译主进程优先用轻量编译后的patch。找适宜的机遇做全量编译(适宜机遇:我的APP退倒后台,其他APP在前台的时分/锁屏)体系做background dexopt也是体系不用的时分去做
防止全量编译

有三种计划:

  1. Atlas计划:在Native侧修正Art虚拟机的履行形式,直接用DexFile底层接口加载Dex文件(影响同进程下的dex加载并且DexFile在O版别以上被废弃)存在可用性和兼容问题
  2. Tbs计划:发现假设在new DexClassLoader的时分optDir传入为null的时分会置空oat_location就不会对你做全量编译(8.0上体系会疏忽你传入的这个途径)
  3. Tinker计划:dexopt便是履行虚拟机的一个指令行,所以在你体系触发全量编译之前手动去调用dex2oat指令履行编译办法intercept-only只做清凉的编译。先用你轻量编译到达的成果首次发动或许首次装置完今后的运转作用和虚拟机相同的作用让他先跑起来也是呈现问题之后的优化计划

Android N混合编译对热修正影响

混合编译:AOT,解释,JIT三种形式并存。

用户真实使用到的类可能只要很少部分,咱们为什么要为了百分之二三十的代码去做全量编译呢?没有必要

N之前的Art虚拟机上装置是做的全量编译,所以装置的时分会等好久,做Jit及时编译又会很慢

在N上处理了这个问题经过混合编译缩短装置时间,体系OAT升级更快: 装置和首次发动用intervept-only的办法没有编译(和davlik虚拟机相同的作用),对哪些代码做编译,什么时分做编译呢:来看N上的增量编译进程:

Android N虚拟机增量编译进程

虚拟机遇在APP代码运转进程中搜集运转到的代码放到profile文件上,体系会经过jobSchedule发动BackgroundDexOptService。这个Service会在灭屏/充电的状态下发动。晚上睡觉或许其他手机空闲的状况时就会启动任务把搜集到的代码给编译好(这些热代码是经常跑的所以会快)。后边发动的时分就会很块,经过这种办法给APP做增量的编译。编译完之后会生成base.odex和base。art(称之为App的image)

虚拟机以为这是热代码所以在你APP发动的时分就提早帮你吧这部分代码加载起来。在ClassLoader创立ClassLinker的时分一次性加载到dexcache上

所以便是你刚发动Application里边什么都还没做就现已加载了一些类(曾经编译好的热代码)

Art混合编译对热修正的三种状况影响剖析

  • 要修正的类不在appimage中: Dex门户选用的是双亲派遣预期的是经过parent去加载假设你要修正的类正好不在appimage里边也便是没有被提早加载那么这个机制就没错补丁能够生效
  • 要修正的类有一部分在appimage中: 假设你有一部分在appimage里边。就导致一部分用的新的,一部分用的老的。这样拜访就会呈现地址紊乱呈现crash
  • 要修正的类现已在appimage中:假设你悉数都在appimage里边,你修正的这些正好之前都被搜集了,那么你这个patch是不会生效的

处理计划

在N以上的设备抛弃设置parent的形式,做全量的直接替换吊咱们的pathclassloader而不是设置他的parent

完结过程

  1. 创立补丁dex的DexClassLoader
  2. 经过contextimpl拿到loadkedApk在拿到持有的PathClassLoader对象。便是体系帮咱们创立的pathClassloader
  3. 经过反射替换这个特点为补丁的classloader

原理:因为体系的appimage提早加载是加载到体系的pathClassloader缓存上的。而咱们后续运转的是用咱们替换的classloader,所以这个新的classloader上没有了appimage的存在了

影响:因为没有了appimage的存在所以功能上会有牺牲可是是能到达修正的目的,计算下来影响是十分小的

本文正在参与「金石计划 . 分割6万现金大奖」