Android类加载(一)——DVM、ART、Dexopt、DexAot名词解析
Android类加载(二)——双亲委托机制
Android类加载(三)——源码解读
从上一篇文章我们知道,Android中类加载器的继承关系如下图:
那么DexClassLoader和PathClassLoader是怎么实现类加载机制的呢?
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
public class PathClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code PathClassLoader} that operates on a given list of files
* and directories. This method is equivalent to calling
* {@link #PathClassLoader(String, String, ClassLoader)} with a
* {@code null} value for the second argument (see description there).
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
* Creates a {@code PathClassLoader} that operates on two given
* lists of files and directories. The entries of the first list
* should be one of the following:
*
*
* - JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
*
- Raw ".dex" files (not inside a zip file).
*
*
* The entries of the second list should be directories containing
* native library files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
在类DexClassLoader和类PathClassLoader,我们只看到了它们自身的构造方法,并没有真正去实现类加载的地方,所以,我们去看它们的父类BaseDexClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, 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里我们也没有LoadClass的方法,所以我们直接去ClassLoader里去看,
/**
* Loads the class with the specified name. Invoking this method is
* equivalent to calling {@code loadClass(className, false)}.
*
* Note: In the Android reference implementation, the
* second parameter of {@link #loadClass(String, boolean)} is ignored
* anyway.
*
*
* @return the {@code Class} object.
* @param className
* the name of the class to look for.
* @throws ClassNotFoundException
* if the class can not be found.
*/
public Class> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
/**
* Loads the class with the specified name, optionally linking it after
* loading. The following steps are performed:
*
* - Call {@link #findLoadedClass(String)} to determine if the requested
* class has already been loaded.
* - If the class has not yet been loaded: Invoke this method on the
* parent class loader.
* - If the class has still not been loaded: Call
* {@link #findClass(String)} to find the class.
*
*
* Note: In the Android reference implementation, the
* {@code resolve} parameter is ignored; classes are never linked.
*
*
* @return the {@code Class} object.
* @param className
* the name of the class to look for.
* @param resolve
* Indicates if the class should be resolved after loading. This
* parameter is ignored on the Android reference implementation;
* classes are not resolved.
* @throws ClassNotFoundException
* if the class can not be found.
*/
protected Class> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
从loadClass里我们可以看到,第一步先调用findLoadedClass去查找是否已经加载过class,如果先前加载过,直接返回对应的class,如果没找到,调用
parent.loadClass(className, false);,如果父亲找到了,直接返回,如果父亲没找到,就调用类自己的findClass,如果找到了就返回,没找到就报异常。(这段代码很好的解释了双亲委托机制)
我们先看findLoadedClass方法
/**
* Returns the class with the specified name if it has already been loaded
* by the VM or {@code null} if it has not yet been loaded.
*
* @param className
* the name of the class to look for.
* @return the {@code Class} object or {@code null} if the requested class
* has not been loaded.
*/
protected final Class> findLoadedClass(String className) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, className);
}
我们在点进VMClassLoader.findLoadedClass(loader, className);
native static Class findLoadedClass(ClassLoader cl, String name);
我们发现findLoadedClass是一个native方法,我们知道native是由C和C++实现的,大概我们也知道,就是把dex文件转化成class文件。我们只要知道被加载过的Class是有缓存的就OK了。我们再看parent.loadClass(className, false);它是一直递归调用parent的loadClass方法,直到找到或者找不到才停止。最后我们看到这个类自己的findClass方法,我们知道DexClassLoader和PathClassLoader都是派生于BaseDexClassLoader的,而且它们本身没有实现findClass方法,所以我们定位到BaseDexClassLoader里的findClass方法。
@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;
}
在方法中看到,Class c = pathList.findClass(name, suppressedExceptions);这段代码,我们点进pathList,发现pathList是BaseDexClassLoader的成员变量private final DexPathList pathList;
那么pathList是怎么生成的呢?我们在BaseDexClassLoader的构造方法里可以看到
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
this.pathList是通过当前类的this,dexPath:dex目录,libraryPath:so库文件所在目录,optimizedDirectory:opt优化后dex所在的目录生成的。
那么pathList.findClass(name, suppressedExceptions);是怎么实现的呢?
所以我们找到DexPathList这个类的findClass方法。
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class findClass(String name, List suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
/**
* Element of the dex/resource file path
*/
/*package*/ static class Element {
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
private ZipFile zipFile;
private boolean initialized;
public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
this.dir = dir;
this.isDirectory = isDirectory;
this.zip = zip;
this.dexFile = dexFile;
}
从这段代码我们可以看出,一开始先通过for循环Element数组,取出数组中每个Element对象(每个Element对象都包含一个DexFile对象),再调用Element的loadClassBinaryName方法获得每个Element对象所对应的class,如果找到了就返回,没找到就返回null,我们发现loadClassBinaryName是在DexFile类里,我们点进去看,最终是定位在是一个native方法上,所以每个Element对象所对应的class是在android底层通过C和C++代码实现的。那么Element数组是怎么创建出来的呢?
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
......
.........
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
我们发现Element数组是在DexPathList的构造方法里通过makePathElements方法创建的,所以我们进入makePathElements看看
/**
* Makes an array of dex/resource path elements, one per element of
* the given array.
*/
private static Element[] makePathElements(List files, File optimizedDirectory,
List suppressedExceptions) {
List elements = new ArrayList<>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
// We support directories for looking up resources and native libraries.
// Looking up resources in directories is useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else if (file.isFile()) {
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if
* the zip file turns out to be resource-only (that is, no classes.dex file
* in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(dir, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
在代码中我们看,首先是for循环dex文件的数组,我们知道dex是文件,所以我们进入else if (file.isFile()) 这个判断里,我们再看到 if (name.endsWith(DEX_SUFFIX)) {,这个判断的意思就是如果name是DEX_SUFFIX 结尾的,进入这个判断,我们通过private static final String DEX_SUFFIX = ".dex";发现DEX_SUFFIX 就是.dex。所以这个判断就是如果是.dex结尾的文件进入这个判断,执行dex = loadDexFile(file, optimizedDirectory);这段代码,所以我们再进入loadDexFile方法
/**
* Constructs a {@code DexFile} instance, as appropriate depending
* on whether {@code optimizedDirectory} is {@code null}.
*/
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
loadDexFile最终是返回DexFile.loadDex,返回的是一个DexFile类型的文件。
所以说,在android中得到一个dex文件的地址,怎么把这个dex文件加载到虚拟机中来,就是通过DexFile来进行加载。其实在DexFile定义了很多native方法,最终加载就是通过这些native方法来实现的。
最终结论:ClassLoader最终是通过DexFile来实现的类加载,里面最重要的就是一个Element数组,这个数组中的每个Element包含一个DexFile对象,DexFile对象其实就代表一个要加载的dex文件。
其实热修复就是通过在Element数组前面插入新的dex文件来实现修复bug的。因为,在不同的dex文件中有相同的类存在时,那么会优先选择排在前面的dex文件中的类。所以,热修复时,会把有问题的类打包到一个dex文件中,然后把这个dex文件插入到Element数组的最前面。