一. 类加载器
Android中的类加载器中主要包括三类BootClassLoader(继承ClassLoader),PathClassLoader和DexClassLoader。后两个继承于BaseDexClassLoader。
1.BootClassLoader
:主要用于加载系统的类,包括java和android系统的类库。(比如TextView,Context,只要是系统的类都是由BootClassLoader加载完成)。
通过打印TextView.class.getClassLoader()即可验证
2.PathClassLoader
:主要用于加载我们应用程序内的类。路径是固定的,只能加载
/data/app中的apk,无法指定解压释放dex的路径,无法动态加载。对于我们的应用默认为PathClassLoader
通过打印getClassLoader()以及ClassLoader.getSystemClassLoader()即可验证
3.DexClassLoader
:可以用来加载任意路径的zip,jar或者apk文件。可以实现动态加载。
简单看一下这两个类的源码:
DexClassLoader类的源码如下:
package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
//但是api26以上 这个函数源码如下 也就是第二个参数已经没有影响
// * @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
PathClassLoader类的源码如下:
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
参数意义
dexPath :需要被加载的jar/apk/dex 文件地址,可以多个,用File.pathSeparator分割。
optimizedDirectory:因为加载apk/jar的时候会被编译器优化解压出dex文件,这个路径就是保存dex文件的。但在api26以上这个参数默认也为null。
libraryPath:库lib文件的路径
parent:给DexClassLoader指定父加载器
可以发现PathClassLoader和DexClassLoader源码很简单,只包含了一个构造函数,去调用父类BaseDexClassLoader。(所有的工作都应该是在BaseDexClassLoader里完成的了。)而这两个加载器不同的是PathClassLoader的构造中少了optimizedDirectory这个参数,原因是PathClassLoader是加载/data/app中的apk,也就是系统中的apk,而这部分的apk都会解压释放dex到指定的目录:/data/dalvik-cache中,这个操作由系统完成,不需要单独传入路径,而DexClassLoader传入,用来缓存需要加载的dex文件,并创建一个DexFile对象,如果为null,会直接使用dex文件原有路径创建DexFile;这个参数已经弃用,自API26起无效;
二、 DexPathList
接下来具体看一下BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
}
在BaseDexClassLoader里我们可以看到根据传入的地址参数构造了一个DexPathList对象。从findClass方法可以看出来加载的类都是从pathList中查找。【findclass方法】是BaseDexClassLoader这个类的核心。那接下来看一下DexPathList类
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
...
}
这里的重点是通过makeDexElements方法得到dexElements集合。而splitDexPath方法是将传入的文件集合转化为一个文件File合集,因为我们上面提到了dexPath可以是多个,用文件分隔符连接即可。
private static Element[] makeDexElements(ArrayList files, File optimizedDirectory, ArrayList suppressedExceptions) {
// 1.创建Element集合
ArrayList elements = new ArrayList();
// 2.遍历所有dex文件(也可能是jar、apk或zip文件)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 如果是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.将Element集合转成Element数组返回
return elements.toArray(new Element[elements.size()]);
}
总体来说,DexPathList的构造函数是将一个个的程序文件(可能是dex、apk、jar、zip)先通过loadDexFile转变成dex,然后封装成一个个Element对象,最后添加到Element集合中。BaseDexClassLoader的findclass方法也就是进一步,我们可以继续看DexPathList的findClass()方法了:
public Class findClass(String name, List suppressed) {
for (Element element : dexElements) {
// 遍历出一个dex文件
DexFile dex = element.dexFile;
if (dex != null) {
// 在dex文件中查找类名与name相同的类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
对Element数组进行遍历(也就是对每一个dex文件遍历),一个dex文件有很多类,通过调用DexFile的loadClassBinaryName找到与name相同的类返回,否则为null。正是这个特性!!我们可以把布丁dex作为Element数组的首个元素。这个就可以动态修复bug了!!【MultiDex方案以及由此衍生出的QQ空间热更新方案都是通过改变dexElements数组的元素位置来实现的】
结合图示
三、 双亲委派机制
如何理解Android ClassLoader的双亲代理/委派机制呢?ClassLoader的loadClass方法保证了双亲委派机制,那我们先看一下这个方法:
public Class> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class> clazz = findLoadedClass(className);//1
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);//2
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);//3
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
1. 首先调用findLoadedClass看自身是否加载过该name的类文件。
2. 如果没有,调用父ClassLoader的loadClass看是否加载过类文件。
3. 如果父classLoader也没有加载过,表明我们这个类从来没有没加载过,则调用自身的findClass方法去dex文件中查找这个类。(联系我们上一节BaseDexClassLoader的findClass方法)
双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器(BootClassLoader根加载器 加载器的顶端),只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
总结说:
1. 什么是双亲委派机制:
ClassLoader在加载一个字节码时,首先会询问 当前的
ClassLoader是否已经加载过此类,如果已经加载过就直接返回,不在重复的去
加载,如果没有的话,会查询它的parent是否已经加载过此类,如果加载过那
么就直接返回parent加载过的字节码文件,如果整个继承线路上都没有加载过
此类,最后由子ClassLoader执行真正的加载。
2. 这样做的好处:
如果一个类被位于树中的任意ClassLoader节点加载过,就会缓存在内存里,那么在以后的整个系统的生命周期中这个类都不会在被重新加载,大大提高了加载类的效率。同样还能类隔离,防止其他类冒充系统类。
3. 什么样的类可以说是同一个类?
包名类名相同以及要被同一个类加载加载过。三个条件都满足,才能说是同一个类。