【Android】Android中的ClassLoader


Java中的ClassLoader

任何的Java程序都是由若干的.class文件组成的完整Java程序。程序运行时需要将.class文件加载到JVM中使用。而负责加载.class文件的就是ClassLoader机制。

img

ClassLoader的作用简单来说就是加载.class文件,提供给程序运行时使用。

双亲委托模型(Parent Delegation Model)

我们看到JDK中ClassLoader的构造方法,需要传入一个父类的加载器,并且持有这个引用:

当类加载器收到加载类或者资源的请求时,通常先委托给父类加载器,当父类加载器找不到指定的类或者资源的时候,才会交由自身执行类架子啊过程。

具体过程如下:

  1. 源ClassLoader先判断这个Class是否已经加载,如果已经加载过,则直接返回Class,没有则委托父类加载器
  2. 以此类推直到始祖加载器(引用类加载器)
  3. 如果始祖加载器发现没有加载过这个Class,则从对应路径下找到对应的class字节码文件载入,如果载入失败,则交给子类加载器
  4. 以此类推直到源ClassLoader
  5. 源ClassLoader如果载入成功,则直接返回Class,如果载入失败,则不会再委托其子类加载器,而是抛出异常

Android中的ClassLoader

Android中的Dalvik/ART虚拟机也是需要加载class文件到内存中使用,但在ClassLoader的加载细节上与传统JVM有所区别。

dex文件

应用打包为apk时,class文件会被打包为一个或多个的dex文件。当我们把apk的后缀名改为zip后(apk本质上就是zip文件),里面会有class文件及dex文件。而MulitiDex解决65535问题就是通过生成多个dex文件。

Android安装应用时会根据不同的CPU平台对Dex做出不同的优化。这个过程由一个名为DexOpt的工具处理。DexOpt是第一次加载Dex时执行的,会生成一个ODEX文件。这种ODEX文件的执行效率是比普通Dex文件要高很多的,通过这样一种方式,就可以加快App的启动和相应速度。

而class文件被打包为dex文件,则是通过SDK中的一个叫dx.jar的工具进行的,我们可以在class文件的根目录下用如下的命令将其打包为dex

dx --dex --output=XXX.dex XXX.class

Android中的Dalvik/ART是无法像普通JVM一样直接加载class文件的,只能通过dex来加载。因此,Android中ClassLoader的工作交给了BaseDexClassLoader来处理。

下面的几篇文章详细讲述了ODEX

BaseDexClassLoader

ClassLoader是一个抽象类,其具体实现有两个,分别是BaseDexClassLoaderSecureClassLoader

SecureClassLoader的子类是URLClassLoader,它只能加载jar文件,在Android中无法使用。

BaseDexClassLoader的子类分别是PathClassLoader和DexClassLoader。

PathClassLoader

PathClassLoader在应用创建的时候,从data/app/…的安装目录下,加载apk文件。它有两个构造函数,并且遵从了双亲委托模型:

其中有两个参数我们需要解释一下:

  • dexPath:它是一个包含了dex的jar文件或者apk文件的路径集,多个路径用分割符号隔开。分隔符通常为":"。
  • libraryPath:它是一个包含了C/C++库的路径集,多个路径也是一样的处理方法。
  • parent:父类加载器,遵从双亲委托模型

PathClassLoader中只有这两个构造方法,具体的实现都是在BaseDexClassLoader中。

BaseDexClassLoader中,dexPath比较受限制,一般是已经安装的apk路径。

其实,在App安装到手机后,apk中的class.dex中的class都是通过PathClassLoader来加载。

DexClassLoader

DexClassLoader对比PathClassLoader来说,PathClassLoader只能加载已经安装应用的dex或者apk文件,而DexClassLoader没有这个限制,可以从SD卡中加载包含class.dex的jar或apk文件(插件化和热修复都是通过这个原来来实现的)。不需要安装应用,即可完成dex的加载。

DexClassLoader中只有一个构造方法,仍然遵循双清委托模型

我们需要解释一下参数

  • dexPath:包含了class.dex的apk、jar路径,多个用分隔符隔开。
  • OptimizedDirectory:用于缓存优化的dex文件路径,就是从jar或apk中提取出来的dex文件路径,不可为空,应该是应用私有的,有读写权限的路径(外部存储空间也可以,但有代码注入的风险)。通过下面的方式我们可以创建一个路径(API21以上):

  • libraryPath:存储C/C++文件的路径集。
  • parent:父类加载器,遵从双亲委托模型。

刚刚我们说到,DexClassLoader和PathClassLoader都是对BaseDexClassLoader的一层简单的封装,真正的实现都在BaseDexClassLoader中。

BaseDexClassLoader源码分析

其结构如下:

img

其中,有一个很重要的字段pathList。它继承了ClassLoader,实现了findClass()findResource()方法。

重要的部分都在DexPathList中了,它的构造方法和之前的类似:

它调用了makePathElements()方法生成了Element数组,Element是DexPathList的嵌套类,有下面的字段:

关于如何生成Element数组,我们可以看到makePathElement的源码:

其中,loadDexFile()方法会调用Native层的方法来读取dex文件,详细的可以看 从源码分析 Android dexClassLoader 加载机制原理这篇文章。

接下来我们看到DexPathList的findClass()方法,它根据传入的完整类名加载对应的class。

这里的话有一个热修复的实现原理的点,我们可以将热修复的补丁dex文件放到dexElements数组前面,这样加载class的时候,会优先找到补丁包的dex文件,加载到class后不再寻找,原来的apk中的同名类就不再使用。

这样,BaseDexClassLoader寻找class路径就很清晰了:

  1. 传入一个完整的类名,调用 BaseDexClassLader 的 findClass(String name)方法
  2. BaseDexClassLader 的 findClass 方法会交给 DexPathList 的 findClass(String name, List<Throwable> suppressed方法处理
  3. 在 DexPathList 方法的内部,会遍历 dexFile ,通过 DexFile 的 dex.loadClassBinaryName(name, definingContext, suppressed) 来完成类的加载

注意事项

在项目中使用BaseDexClassLoader或者DexClassLoader加载dex或者class时,无法调用findClass()方法,它有包访问权限,我们需要调用loadClass(String className),它其实是BaseDexClassLoader的父类ClassLoader实现的。


Android Developer in GDUT