热修复之类加载机制总结

扩展
了解JVM中的类加载机制 及双亲委托模式,
之后你会发现Android的ClassLoader与Java中的不同之处,因着前者加载的是dex文件,并非是class字节码文件,再去学习dex相关概念知识,


Android类加载介绍?
Android中的ClassLoader类加载机制主要用来加载dex文件,系统提供了PathClassLoader、DexClassLoader两个API可供选择。
ClassLoader种类如下:
BootClassLoader,BaseDexClassLoader:父类
PathClassLoader:只能加载已安装到Android系统的APK文件;
DexClassLoader:支持加载外部的APK、Jar或dex文件;(所有的插件化方案都是使用它来加载插件APK中的.class文件,也是动态加载的核心依据!)
如上,在简单理解之后发现Android的ClassLoade和Java的大体上是一一对应的,只不过内部实现有些变化。
思考一个问题,一个App正常运行最少需要哪些ClassLoade?
答案揭晓:最少需要BootClassLoader和PathClassLoader。首先BootClassLoader是无可或缺的,因为它需要加载framework层的一些class文件,而PathClassLoader用来加载已安装到系统上的文件。

1.基于ClassLoad的 热修复实现
原理:主要是基于classLoader的热修复。
在android中有两个常用ClassLoader,两个有一个共同的父类BaseDexClassLoader(父类是ClassLoader),
PathClassLoader加载已安装apk中class,DexClassLoader加载未安装apk或者aar中class。
其中PathDexLoader用来加载系统类和应用类;
DexClassLoader用来加载一些jar、apk、dex文件,其实jar和apk文件实际上加载的都是dex文件。

在BaseDexClassLoader->DexPathList类-> Element[] dexElements( 存储着apk或者aar中所有dex的集合)。

class加载类是从头遍历这个集合找到class就返回不会再往下找,这样我们就可以把修改好的dex查在数组的前边,让类加载器选择我们修改好的class(不知道算不算是一个bug)。
热修复原理: ClassLoader会遍历一个由dex文件组成的数组,然后加载其中的dex文件,
我们会把正确的dex(修复过的类所在的dex)文件插入数组的前面, 当加载器 加载到好的类文件时候就不会加载有bug的类了,就实现了热修复

热修复的原理
我们知道Java虚拟机 —— JVM是加载类的class文件的,而Android虚拟机——Dalvik/ART VM是加载类的dex文件,
而他们加载类的时候都需要ClassLoader,ClassLoader有一个子类BaseDexClassLoader,而BaseDexClassLoader下有一个
数组——DexPathList,是用来存放dex文件,当BaseDexClassLoader通过调用findClass方法时,实际上就是遍历数组,
找到相应的dex文件,找到,则直接将它return。而热修复的解决方法就是将新的dex添加到该集合中,并且是在旧的dex的前面,
所以就会优先被取出来并且return返回。

---
修复的步骤为:
可以看出是通过获取到当前应用的Classloader,即为BaseDexClassloader
通过反射,获取到他的DexPathList属性对象pathList
通过反射,调用pathList的dexElements方法把patch.dex转化为Element[]
两个Element[]进行合并,把patch.dex放到最前面去
加载Element[],达到修复目的。

1.几个概念介绍:

BaseDexClassLoader源码分析:
以上DexClassLoader、PathClassLoader两个类源码没有具体实现,最大的区别在于后者只能加载已安装于应用的dex文件,而详情部分还是要参数它们的父类——BaseDexClassLoader。
BaseDexClassLoader类重点源码部分,类中只有一个成员变量DexPathList,继续查看其构造方法,其中创建了DexPathList对象,传入了四个参数,分别是:
DexClassLoader:父类加载器本身;
dexPath: 需要加载的dex文件路径;
librarySearchPath: 包含native库的目录列表(可能为null);
optimizedDirectory: dex文件需要被写入的内部目录(可能为null);
BaseDexClassLoader 构造方法中的这些参数是其子类传过来的,
只是对于在其构造方法中只做了一件事——创建DexPathList对象,有些不解。
继续查看重点方法findClass(String name),重点部分笔者用红框圈出来了,通过成员变量dexList的findClass 加载获取的类返回,若类为null则报错,此处意味着真正执行加载类的重点部分并非是BaseDexClassLoader,它也只是一个中介,真相在于DexPathList类,继续延伸查看此类。

DexPathList类介绍:
成员变量:dexElements: Element[]类型,Element是一个内部类。此类作用就是指定dex/resource/native 库路径,其内部重要成员DexFile的dexFile,这是dex文件在Dalvik安卓虚拟机中的具体实现。后续成员变量类型类似 不再赘述……
DexPathList类的makeElements()核心作用就是:
将指定加载路径dexPath的所有文件遍历获取dex文件,并转换成DexFile类型存储到Element数组中。(Element数组的作用是为了后续DexPathList类的findClass方法铺垫)。

DexPathList的findClass():查找dex,
作用就是遍历之前makeElements方法中存储好的Element数组,将Element类型转换为DexFile类型,调用DexFile的内部方法loadClassBinaryName(),在dex文件中查找获取拼接成class字节码文件返回。

Element: 内部封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element。
DexFile类: 主要作用就是创建DexFile对象返回,
调用DexFile的内部方法loadClassBinaryName()--> defineClassNative(name, loader, cookie, dexFile),
因此, 也可以推理出defineClassNativenative方法是通过C/C++ 在dex文件中查找获取拼接成class字节码文件返回。

dex: dex文件格式,一个dex文件中存储了整个工程中所有的class文件,其文件数据存储在dex文件中的“数据区”。
===================

--通过反射 操作得到PathClassLoader的DexPatchList, 反射调用patchlist的makeDexElements()方法,
把本地的dex文件直接替换到Element[]数组中去,达到修复的目的。

多个Element组成了有序的Element数组dexElements。当要查找类时,会在注释1处遍历Element数组dexElements(相当于遍历dex文件数组),注释2处调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。如果在Element中(dex文件)找到了该类就返回,如果没有找到就接着在下一个Element中进行查找。
根据上面的查找流程,我们将有bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换之前存在bug的Key.class,排在数组后面的dex文件中的存在bug的Key.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案。

2.结合以上Android类加载时序图,再次回顾一下ClassLoader源码的解读研究过程?
首先类的加载是在ClassLoader类的loadClass 方法中进行,此方法会判断此类是否被自己或双亲加载过(这也是著名的“双亲委派模式”);
若加载过则无需重复load,直接返回类实例;
否则调用findClass方法寻找获取这个类,可是findClass方法在ClassLoader类中是一个空实现,真正实现是在BaseDexClassLoader类中;
而BaseDexClassLoader类也未具体实现,调用的实则是DexPathList类中的findClass方法;
DexPathList类中 findClass方法最终又调用DexFile中的defineClassNative ,DexFile的一个native方法来完成主要类加载逻辑。
以上是类加载过程涉及到的几个类中方法互相调用最终实现“类加载”的过程,

以下是重点方法中实现的逻辑总结:
首先在DexPathList类的构造方法中:将所有的dex文件(File类型)转换成DexFile类型,并且将其转化为Element数组,便于findClass方法逻辑处理,
然后在findClass 方法中遍历Element数组(Element类型中存储着DexFile类型),获取Element中的DexFile,
调用DexFile的内部方法loadClassBinaryName,在dex文件中查找获取拼接成class字节码文件返回(loadClassBinaryName是一个 native方法)。


而这整个过程,一系列方法、类之间调用的核心逻辑是:通过指定加载dex路径中,遍历文件找到dex文件,然后在存储了整个工程class文件数据中的dex文件中,查找搜索并拼接 class字节码文件返回。

----
问题3:类加载修复方案对比?

QQ空间的超级补丁和Nuwa是按照上面说得,将补丁包 放在Element数组的第一个元素得到优先加载。
--微信Tinker:
微信Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素。
微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,
区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的class.dex文件,以达到修复的目的。
--通过反射 操作得到PathClassLoader的DexPatchList, 反射调用patchlist的makeDexElements()方法,
把本地的dex文件直接替换到Element[]数组中去,达到修复的目的。
对于如何进行patch.dex与classes.dex的合并操作,这里微信开启了一个新的进程,开启新进程的服务TinkerPatchService 进行合并。


如果Key.Class文件中存在异常,将该Class文件修复后,将其打入Patch.dex的补丁包
(1) 方案一:
通过反射获取到PathClassLoader中的DexPathList,然后再拿到 DexPathList中的Element数组,将Patch.dex放在Element数组dexElements的第一个元素,最后将数组进行合并后并重新设置回去。在进行类加载的时候,由于ClassLoader的双亲委托机制,该类只被加载一次,也就是说Patch.dex中的Key.Class会被加载。

(2)方案二:
提供dex差量包patch.dex,将patch.dex与应用的classes.dex合并成一个完整的dex,完整dex加载后得到dexFile对象,作为参数构建一个Element对象,然后整体替换掉旧的dex-Elements数组。(Tinker)


问题2:Dex插桩原理:
ClassLoader 是通过调用 findClass 方法,在 pathList 对象中的 dexElements[] 中遍历dex文件寻找相关的类。由于靠前的dex会优先被系统调用,所以就有了插桩的概念。将修复好的 dex 插入到 dexElements[] 的最前方,这样系统就会调用修复好的插入类而不是靠后的 bug 类。
上图中,patch.dex 是插入的 dex ,classes2.dex 是原有的 bug dex。ClassLoader 在遍历时优先获取了 patch.dex 中的 D.class ,所以 classes2.dex 中的 D.class 就不会被调用,这样就完成了对 D.class 的替换,修复了bug。
本文简单介绍了代码修复的技术原理,下篇文章将从系统源码入手,结合我自己封装的代码修复开源框架Fettler,详细解读代码修复的每一个过程。


问题1:类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启呢?这是因为类是无法被卸载的,因此要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。

-----

 

原文地址:https://www.cnblogs.com/awkflf11/p/12624149.html