前言
任何一个java
程序都是由一个或者多个class
文件组成,在程序运行时,需要将class
文件加载到JVM
中才可以使用,负责加载这些class
文件的就是java
的类加载机制。ClassLoader
的作用简单的来说就是加载class
文件,提供给程序运行时使用,每个Class
对象的内部都有一个ClassLoader
字段来标识自己是由哪个Classloader
加载的。
Java与Android类加载机制的区别
我们都知道Java
中JVM
虚拟机加载的是Class
文件,而DVM
和ART
加载的是Dex
文件,所以java
的类加载器和Android
的类加载器是不一样的。Java
中的类加载器主要有系统加载器和自定义加载器两种类型。系统类加载器主要是Bootstrap ClassLoader
、Extensions Classloader
和Application Classloader
这3
种。Android
中的Classloader
类型和java
中一样,也分为系统加载器和自定义加载器两种。系统类加载器主要包括3
种,分别是BootClassloader
、PathClassloader
和DexClassLoader
这三种,接下来我们就来简单的了解一下Android
中的类加载器。
Android中的类加载器
-
BaseDexClassLoader:实现应用层类文件的加载,真正的加载逻辑委托给
PathList
来完成。 -
PathClassLoader:继承自
BaseDexClassLoader
,加载系统类和应用程序的类,通常用来加载已安装的apk
的dex
文件,实际上外部存储的dex
文件也能加载。 -
DexClassLoader:继承自
BaseDexClassLoader
,可以加载dex
文件以及包含dex
的压缩文件(apk,dex,jar,zip)
,不管加载哪种文件,最终都要加载dex
文件。Android8.0
之后和PathClassloader
无异。 -
BootClassLoader:
Android
系统启动时会使用BootClassLoader
来预加载常用类,它继承自ClassLoader
,是顶层的父加载器parent
。
PathClassLoader & DexClassLoader的异同
PathClassLoader构造方法:
public PathClassLoader(String dexPath,String librarySearchPath,ClassLoader parent){
super(dexPath,null,librarySearchPath,parent);
}
DexClassLoader构造方法:
//android 8.0之前
public DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath,ClassLoader parent){
super(dexPath,new File(optimizedDiretory),librarySearchPth,parent);
}
//android 8.0之前
public DexClassLoader(String dexPath,String optimizedDirectory,String librarySearchPath,ClassLoader parent){
super(dexPath,null,librarySearchPath,parent);
}
- dexPath:
dex
文件以及包含dex
的apk
文件或者jar
文件的路径集合,多个路径用文件分隔符分隔,默认文件分隔符为“:”
。 - optimizedDerectory:
Android
系统将dex
文件进行优化后所生成的ODEX
文件的存放路径,该路径必须是一个内部存储路径。在一般情况下,使用当前应用程序的私有路径:data/data/< Package Name> /...
。 - librarySearchPath: 所使用到的
C/C++
库存放的路径 - parent: 父加载器
从构造方法可以看出DexClassLoader
和PathClassLoader
的实现逻辑基本一样,其实二者都可以加载指定路径的apk、jar、zip、dex
,区别在于DexClassLoader
多了一个optimizedDirectory
参数,optimizedDirectory
参数就是dexopt
的产出目录(odex)
。那PathClassLoader
创建时,这个目录为null
,就意味着不进行dexopt
?并不是,optimizedDirectory
为null
时的默认路径为:/data/dalvik-cache
。optimizedDirectory
这个参数在API26
的时候被谷歌废弃掉了,可以看到DexClassLoader
中即使传递了这个参数,在super
调用中,传递的值也是null
,而且查看PathClassloader
和DexClassLoader
的super
调用,会发现代码是一样的,那么也就是说在Android8.0
之后,这两个ClassLoader
是没有区别的。
dex和odex区别: 一个APK
是一个程序压缩包,里面有个执行程序包含dex
文件,ODEX
优化就是把包里面的执行程序提取出来,就变成ODEX
文件。因为你提取出来了,系统第一次启动的时候就不用去解压程序压缩包,少了一个解压的过程。这样的话系统启动就加快了。为什么说是第一次呢?是因为DEX
版本的也只有第一次会解压执行程序到/data/dalvik-cache(针对PathClassLoader)
或者optimizedDirectory(针对DexClassLoader)
目录,之后也是直接读取目录下的的dex
文件,所以第二次启动就和正常的差不多了。当然这只是简单的理解,实际生成的ODEX
还有一定的优化作用。ClassLoader
只能加载内部存储路径中的dex
文件,所以这个路径必须为内部路径。
双亲委托介绍
类加载器在查找Class
的时候采用的就是双亲委托机制,双亲委托就是在加载.Class
文件的时候,首先会判断该Class
文件是否被自身加载,如果没有加载的话会委托给父加载器parent
去进行查找而不是让自身去加载,委托给父加载器之后父加载器会去判断自己是否有加载过这个文件,如果有加载,那么就直接返回,如果这个文件没有被加载的话,那么会继续向上委托给更上一级的父加载器去加载,直到达到链路的顶层ClassLoader
,如果顶层ClassLoader
也没有加载这个文件的话,那么顶层ClassLoader
就会尝试自己去加载这个文件,如果加载失败,就会逐级向下交给它的子加载器加载这个文件,以此类推,如果最后都没有找到的话,才会交给自身去查找,其实双亲委派机制就是一个递归的过程。
Android双亲委派机制的实现
Android
中的ClassLoader
和java
中一样,同样遵循了双亲委托机制来加载,查看Classloader.java
中的loadClass
能够看出:
1、 首先调用findLoadedClass
检查传入的类是否已经被加载,如果已经加载那么就直接返回。
2、 如果第一步中类没有被加载(c == null)
,那么就会判断parent
是否等于null
,也就是判断父加载器是否存在,如果父加载器存在,就调用父加载器的loadClass
方法。
3、 如果父加载器不存在就会调用findBootstrapClassOrNull
,这个方法会直接返回null
。
4、 如果到了第4
步依然c == null
,那么表示在向上委托的过程中,没有加载该类,会调用findClass
继续向下进行查找。
接下来我们引用一个例子再总结说明一下整个加载流程:
上图中创建了一个DexClassLoader
对象,使用DexClassLoader
进行加载一个类,参数中的parent
我们传入了context.getClassLoader
,这里就等于DexClassLoader
的parent
我们给它传的是PathClassloader
,所以此时的父子关系是DexClassLoader——>PathClassLoader——>BootClassLoader
,也就是DexClassLoader
的parent
为PathClassloader
,PathClassloader
的parent
为BootClassloader
,需要注意的是这里的parent
并不是继承关系,比如PathClassloader
继承自BaseDexClassLoader
,但是PathClassloader
的parent
为BootClassLoader
,二者并不冲突。下面使用一张流程图来进行总结。
双亲委派的作用
- 避免重复加载,如果已经加载过一次
Class
,就不需要再次加载,而是直接读取已经加载的Class
。 - 对于任意一个类确保在虚拟机中的唯一性,由加载它的类加载器和这个类的全类名一同确立其在
Java
虚拟机中的唯一性。不同的类加载器加载同一个class
文件得到的不是同一个class
对象 - 安全,保证系统类
.class
文件不能被篡改。通过委托方式可以保证系统类的加载逻辑不会被篡改。假如我们自定义一个String
类来替代系统的String
类,就会造成安全隐患,但是使用双亲委托就会使得系统的String
类在Java
虚拟机启动时就被加载,也就无法通过自定义String
类来替代系统的String
类。 - 只有当两个类名完全一致并且被同一个类加载器所加载的类,
Java
虚拟机才会认为它们是同一个类。
类的加载过程
可以看到在所有父ClassLoader
无法加载Class
时,则会调用自己的findClass()
方法。其实任何ClassLoader
子类,都可以重写loadClass()
与findClass()
。一般如果你不想使用双亲委托,则重写loadClass()
修改其实现。而重写findClass()
则表示在双亲委托下,父ClassLoader
都找不到Class
的情况下,定义自己如何去查找一个Class
。如果没有重写的话,那findClass()
是在BaseDexClassLoader
中实现的。我们来看一下findClass()
中的实现逻辑。
这里省略了部分代码,可以看到调用了pathList
的findClass()
方法,pathList
就是DexPathList
对象,在BaseDexClassLoader
初始化的时候被创建。接下来进入DexPathList
。
可以看到DexPathList
在创建的时候调用了makeDexElements()
方法来创建出了dexElements
数组,在makeDexElements
之前我们先来看一下splitDexPath()
方法,在这个方法中将dexPath
目录下的所有程序文件转变成一个File
集合,而且dexPath
是一个用冒号作为分隔符把多个程序文件目录拼接起来的字符串,如 /data/dexdir1:/data/dexdir2:...
。makePathElements
方法核心作用就是将指定路径中的所有文件转化成DexFile
同时存储到到Element[]
这个数组中。接下来进入DexPathList
的findClass()
方法中。
在findClass()
方法中会通过for
循环不断的遍历dexElement
数组,拿到element
,然后调用element
的findClass()
,Element
是DexPathList
的内部类,dexElements
是维护dex
文件的数组, 每一个Element
对应一个dex
文件。DexPathList
遍历dexElements
,从每一个dex
文件中查找目标类,在找到后即返回并停止遍历。继续往下看,进入element
的findClass()
方法。
如果dexFile
不等于空,就去查找类名与name
相同的类,否则返回null
,dexFile
就是用来描述dex
文件的,Dex
的加载以及Class
的查找,都是由该类调用它的native
方法完成的。