热修复之类加载机制总结
Posted awkflf11
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了热修复之类加载机制总结相关的知识,希望对你有一定的参考价值。
扩展:
了解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,因此采用类加载方案的热修复框架是不能即时生效的。
-----
1.Java类加载机制 双亲委托模式
加载阶段
类加载阶段是由类加载器负责根据一个类的全名类读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区内,然后将其转换为一个与目标类型对应的java.lang.Class
对象实例,这个Class对象在日后就会作为方法区中的该类的各种数据的访问入口。
JVM支持两种类型的类加载器,分别为引导类加载器(BootStrap ClassLoader) 和自定义类加载器(User-Defined Classloader)我们常用的包括
Extension ClassLoader、Application ClassLoader这三个类加载器。
BootStrap ClassLoader
BootStrap ClassLoader也称之为启动类加载器,由C++语言编写,并嵌套在JVM内部,主要负责加载JAVA_HOME/lib
目录中的所有类型。
Extension ClassLoader
ExtClassLoader派生于ClassLoader, 采用Java语言编写,负责加载ext文件夹(jrelibext)内的类
Application ClassLoader
AppClassLoader派生于ClassLoader, 采用Java语言编写,负责加载 应用程序级别的类路径,提供的环境变量路径等
双亲委托模式
一种被JVM设计者制定的类加载器的加载机制。按照双亲委托模式的规则,除了启动类加载器之外,程序中每一个类加载器都应该拥有一个超类加载器,比如Application ClassLoader
的超类加载器就是Extension ClassLoader,开发人员自定义的加载器的超类就是Application ClassLoader,当一个类加载器收到一个加载任务时,并不会立即展开加载
,而是将加载任务委派给它的超类加载器去执行,每一层的加载器都采用这种方式,直到委派给顶层的启动类加载器为止,如果超类无法加载该类,则会将类的加载内容退回给它的下一层
加载器去加载。双亲委托模式的优点就是:能有有效的确保一个类的全局唯一性。
注意:Java虚拟机并没有明确要求类加载器的加载机制一定要使用双亲委托模式,只是建议这样做,而在Tomcat
中,当默认的类加载器接收到一个加载任务时,首先会由
它自动加载,当加载失败,才会将类委派给它的超类加载器去执行,这是Servlet
规范推荐的一种做法。
连接阶段
连接阶段由验证、准备、解析3个阶段构成。
验证
验证主要任务就是验证类信息是否符合JVM规范,是否是一个有效的字节码文件,而验证的内容涵盖了类数据信息的格式验证、语义分析、操作验证等
准备
准备阶段主要任务就是为类中所有静态变量分配内存空间,并为其设置一个初始值(由于对象还没有产生,因此实例变量将不在此操作范围内)
解析
解析阶段主要任务就是将常量池中所有的符号引用全部转换为直接引用,由于Java虚拟机规范中并没有明确要求解析阶段一定要按照顺序执行,因此解析阶段可以等到初始化
以后再执行。
初始化阶段
初始化阶段中,JVM会将一个类中所有被static关键字标识的的代码统统执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖掉之前的准备阶段中JVM为其设置的初始值,
如果执行的是static代码块
JVM就将会执行static代码中的所有操作。
以上是关于热修复之类加载机制总结的主要内容,如果未能解决你的问题,请参考以下文章
Android热修复:底层替换类加载原理总结 及 DexClassLoader类加载机制源码探索
Android热修复:以DexClassLoader类加载原理编写demo实现类替换修复