胡琪

为今天工作,为明天投资,为未来孵化一些东西!

也谈安卓dex的动态加载与MultiDex和热修复

在前面从java虚拟机ClassLoader到安卓dex动态加载之ClassLoader动态加载jar的文章中讲到了java中jar的动态加载,而安卓和java一样都是使用的java语言开发,因此最终文件肯定还是字节码,只不过安卓不在使用java虚拟机来加载这些字节码,而且字节码也不再是.class/jar的文件格式,而是安卓特有的dex文件格式。也正因为这些相似性,java中的类加载机制被安卓给借鉴过来了,只不过安卓实现了自己的类加载器,而不在是java中的那些ClassLoader,因为要加载的文件格式已经不同。在安卓中的类加载器主要包括PathClassLoader和DexClassLoader,这两者都是继承自BaseDexClassLoader,其中PathClassLoader是安卓中的默认加载器。关于这两种加载器的区别这里就不进行赘述了,大家可以在好文转载模块查看相关主题。这里重点讲解一下安卓dex的加载原理,以及基于该原理衍生出来的MultiDex,热修复等技术。

dex加载原理

安卓中PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader,因此我们只需要分析BaseDexClassLoader即可。我们知道加载一个类都是调用的ClassLoader的loadClass方法,而该方法会调用findClass方法。因此我们首先从BaseDexClassLoader的findClass方法入手。该方法源码如下:

可以看到BaseDexClassLoader的findClass方法实际上是调用了pathList这个成员的findClass方法,而pathList是一个DexPathList对象,该成员是在BaseDexClassLoader的构造函数中赋值的,也就是说当实例化一个BaseDexClassLoader对象时也随之创建了一个DexPathList对象,代码如下

所以接下来看看DexPathList类的findClass方法。

可以看到在DexPathList类的findClass方法中会迭代遍历dexElements成员,该成员是一个Element[]类型的数组,其中的每一个元素Element中就包含了DexFile这个数据结构,而DexFile就是安卓中dex文件在java层对应的数据描述,然后调用DexFile类的loadClassBinaryName方法来查找加载某个类,从findClass这个函数可以看到,在循环遍历Element[]中每一个Element中的DexFile时,如果在当前DexFile中加载到了要查找的类,会直接返回该类。否则继续迭代遍历。那么如果多个dex文件中包含了同一个类,肯定会优先返回在Element[]数组中靠前的DexFile中的那个类,注意这很重要,这是后面讲到的QQ空间热修复的原理所在。dex加载流程基本就讲完了,我们再来挖一些细节,我们来看下dexElements这个成员是在哪里赋值的,查看源码发现是在DexPathList的构造函数中赋值的,代码如下

也就是说在构造DexPathList对象的时候会调用makeDexElements方法,该方法会返回一个Element[]数组,该数组中的每一个Element包含了要加载的dex文件在java层对应的描述DexFile,这个函数是非常重要的函数,因此我们看下具体实现:

可以看到该函数会根据不同的文件类型如dex,apk,jar,zip等进行不同的处理,但是最终都会调用loadDexFile函数来将file参数指向的dex文件转换为dex文件在java层对应的数据描述DexFile对象,然后将该对象添加到Element中,最终返回。而loadDexFile函数实际上是调用了DexFile类的loadDex函数,

而loadDex函数实际上也是直接调用DexFile的构造函数。从这个过程可以看到dex的最终加载是DexFile这个类来完成的。但是对于ClassLoader而言,dex的加载是通过DexPathList类的makeDexElements函数来完成的,该函数会根据传入的dex文件的路径(实质参数是dex文件路径所代表的File对象的集合)返回一个与dex文件相关联的包含DexFile的数组elements,而该数组elements正是从dex中加载类时迭代遍历的Element[]数组。既然只需要一个dex文件的路径就可以得到在加载某个类时查找的DexFile。那么如果我们能够通过反射拿到某个ClassLoader的DexPathList对象,然后通过反射调用其makeDexElements函数就可以加载我们我们自己指定的路径的dex文件了,比如加载Assets目录下的插件apk。当然除了通过makeDexElements将指定路径的dex文件加载到内存以外还需要修改原DexPathList类的elements字段的内容。事实上谷歌的MultiDex的原理正式基于此。

总结一下dex的加载过程,这个是理解MultiDex和热修复原理的理论基础。

  1. 当通过PathClassLoader加载某个类时,实质上是调用的BaseDexClassLoader的findClass,而BaseDexClassLoader的findClass实质上调用的是DexPathList类的findClass
  2. 当调用BaseDexClassLoader的findClass方法时已经产生了DexPathList对象(因为当构造BaseDexClassLoader对象的时候就会创建DexPathList对象),而在构造DexPathList对象时会通过该类的makeDexElements函数将dex文件加载到内存,同时返回一个包含java层的DexFile类的Element[]数组dexElements(如果读者还想进一步了解DexFile类是如何加载dex文件到内存的,实质是在native层调用linux系统函数mmap来将文件映射到内存的,详细分析可以看http://www.cnblogs.com/lanrenxinxin/p/4712224.html
  3. 查找类时是从步骤2中得到的dexElements数组中迭代查找,如果找到会直接返回该类,也就是说会优先返回在dexElements数组中靠前的dex文件中的类。

其中步骤2衍生出了谷歌MultiDex原理,步骤3衍生出了QQ空间热修复原理。当然实现的话3个步骤都包含。另外从上述过程可以看到要实现加载指定路径的dex,比如Assets目录下的dex文件,有两种方式:

  • 一种是通过反射获取当前ClassLoader的pathList成员,然后通过反射调用其makeDexElements函数,事实上谷歌的MultiDex就是采用的这种方式
  • 另外一种是使用DexClassLoader,然后通过反射获取该DexClassLoader的pathList,然后通过反射获取pathList的dexElements成员,事实上与QQ空间热修复原理类似的开源的nuwa就是采用的这种方式。这种方式的本质还是调用了makeDexElements函数(在构造DexClassLoader时会创建DexPathList对象,而DexPathList的构造函数中调用了makeDexElements函数)

MultiDex

谷歌MultiDex的源码位于https://android.googlesource.com/platform/frameworks/multidex/+/d79604bd38c101b54e41745f85ddc2e04d978af2/library/src/android/support/multidex

MultiDex的使用请参看https://developer.android.com/studio/build/multidex.html#about

MultiDex的源码不复杂,总共只4个java文件。其中最重要的是MultiDex.java这个类。不管使用哪种方式配置,其本质都是调用了MultiDex的install方法。该方法很长,大家可以参看源码,但最核心的是调用了

而该函数会根据安卓sdk的不同版本,调用不同的install函数来加载额外的dex。

这里以V19为例进行分析,因为其他的都是大同小异,原理都是相同的,只不过不同的版本在细节处理上可能不同而已。

可以看到原理即是基于前面dex加载流程的原理,具体实现是通过反射拿到当前ClassLoader的pathList成员,然后通过反射调用pathList的makeDexElements函数实现加载file参数指向的dex文件,然后将pathList的dexElements设置为调用makeDexElements函数时返回的Elements数组和原ClassLoader已经加载的dex的Elements数组之和,这样就实现了加载多dex的效果。

QQ空间热修复

QQ空间热修复的原理和MultiDex极其相似,只不过在将pathList的dexElements设置为调用makeDexElements函数时返回的Elements数组和原ClassLoader已经加载的dex的Elements数组之和这一步是将patch对应的Elements数组放到原dex对应的Elements数组的前面,这样查找相同的类时就会优先查找到patch中修复的类。从而实现热修复。QQ空间热修复的本质是类替换,在使用同一个类时不在是使用原dex中的类,而是使用优先查找到的patch中对应的类。当然在具体实现过程也有一些细节要进行处理,比如说类校验等,具体的可以参考QQ空间团队官方的博客。【QQ空间技术团队】安卓App热补丁动态修复技术介绍

打赏

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注