###一. Android中ClassLoader的种类
Android的ClassLoader与Java的ClassLoader基本是一一对应的。如果对Java的ClassLoader不是很了解,可以参考《【Java 虚拟机】类加载器》。
- BootClassLoader(Java的BootStrap ClassLoader)
用于加载Android Framework层class文件。 - PathClassLoader(Java的App ClassLoader)
用于加载已经安装到系统中的apk中的class文件。 - DexClassLoader(Java的Custom ClassLoader)
用于加载指定目录中的class文件。 - BaseDexClassLoader
是PathClassLoader和DexClassLoader的父类。
###二. Android中ClassLoader的特点
遵循双亲委派模型
ClassLoader在加载一个class文件时:会询问当前ClassLoader是否已经加载过子类,如果已经加载过则直接返回,不再重复加载。如果没有加载过,会去查询当前ClassLoader的parent是否已经加载过。
因为遵循双亲委派模型,Android中的classLoader具有两个特点:
- 类加载共享
当一个class文件被任何一个ClassLoader加载过,就不会再被其他ClassLoader加载。 - 类加载隔离
不同ClassLoader加载的class文件肯定不是一个。举个栗子,一些系统层级的class文件在系统初始化的时候被加载,比如java.net.String,这个是在应用启动前就被系统加载好的。如果在一个应用里能简单地用一个自定义的String类把这个String类替换掉的话,将有严重的安全问题。
###三. ClassLoader源码详解
我们从ClassLoader.java的loadClass()方法看起。我们知道Android的ClassLoader是实现了双亲委派模型的,我们来从源码角度来看下是如何实现的。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
可以看到,先是判断ClassLoader自身是否加载过该class文件,如果没有再判断父ClassLoader是否加载过,如果都没有加载过再自己去加载。这和我们上述的双亲委派模型思想完全一致。
好了,我们来看下ClassLoader是如何去加载class文件的呢?也就是去看下findClass()方法的具体实现。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
很遗憾,findClass()方法是一个空实现,也就是说它的具体实现是交给子类的。
如图,可以看到DexClassLoader和PathClassLoader是ClassLoader的间接实现类。
所以,下面我们来着重讲解一下DexClassLoader和PathClassLoader的源码。
###四. DexClassLoader、PathClassLoader、BaseDexClassLoader源码详解
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
可以看到DexClassLoader的源码非常简单,只有一个构造方法。我们来看下其四个参数都是什么含义。
- dexPath。要加载的dex文件路径。
- optimizedDirectory。dex文件要被copy到的目录路径。
- libraryPath。apk文件中类要使用的c/c++代码。
- parent。父装载器,也就是真正loadclass的装载器。
我们接下来看PathClassLoader的源码。
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
它的源码也只是有两个构造方法,我们来看第二个构造方法,可以看出,它与DexClassLoader的构造方法的区别就是少了一个要把dex文件copy到的目录路径。正是因为缺少这个路径,我们的PathClassLoader只能用来加载安装过的apk中的dex文件。
这两个ClassLoader的真正核心方法都在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<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
/**
* @hide
*/
public void addDexPath(String dexPath) {
pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
protected synchronized Package getPackage(String name) {
if (name != null && !name.isEmpty()) {
Package pack = super.getPackage(name);
if (pack == null) {
pack = definePackage(name, "Unknown", "0.0", "Unknown",
"Unknown", "0.0", "Unknown", null);
}
return pack;
}
return null;
}
/**
* @hide
*/
public String getLdLibraryPath() {
StringBuilder result = new StringBuilder();
for (File directory : pathList.getNativeLibraryDirectories()) {
if (result.length() > 0) {
result.append(':');
}
result.append(directory);
}
return result.toString();
}
@Override public String toString() {
return getClass().getName() + "[" + pathList + "]";
}
}
可以看到,这个类的实现也是比较简单。我们来分析一下。
我们看到BaseDexClassLoader有一个成员变量DexPathList,其次它的核心方法是findClass()。我们来看下具体实现。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
我们看到,findClass其实是通过成员变量pathList的findClass()方法来查找的。
所以,我们接下来还需要去看DexPathList的源码。
###五. DexPathList源码详解(重点)
由于DexPathList源码较长,我们这里分段讲解。
####1. 构造方法
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
suppressedExceptions,
definingContext);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
构造方法里最重要的一行代码是
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
这行代码的意思是构造一个Element数组。那么Element是什么呢?它是DexPathList的一个内部类。
static class Element {
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
private ClassPathURLStreamHandler urlHandler;
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;
}
@Override public String toString() {
if (isDirectory) {
return "directory \"" + dir + "\"";
} else if (zip != null) {
return "zip file \"" + zip + "\"" +
(dir != null && !dir.getPath().isEmpty() ? ", dir \"" + dir + "\"" : "");
} else {
return "dex file \"" + dexFile + "\"";
}
}
public synchronized void maybeInit() {
if (initialized) {
return;
}
initialized = true;
if (isDirectory || zip == null) {
return;
}
try {
urlHandler = new ClassPathURLStreamHandler(zip.getPath());
} catch (IOException ioe) {
/*
* Note: ZipException (a subclass of IOException)
* might get thrown by the ZipFile constructor
* (e.g. if the file isn't actually a zip/jar
* file).
*/
System.logE("Unable to open zip file: " + zip, ioe);
urlHandler = null;
}
}
public String findNativeLibrary(String name) {
maybeInit();
if (isDirectory) {
String path = new File(dir, name).getPath();
if (IoUtils.canOpenReadOnly(path)) {
return path;
}
} else if (urlHandler != null) {
// Having a urlHandler means the element has a zip file.
// In this case Android supports loading the library iff
// it is stored in the zip uncompressed.
String entryName = new File(dir, name).getPath();
if (urlHandler.isEntryStored(entryName)) {
return zip.getPath() + zipSeparator + entryName;
}
}
return null;
}
public URL findResource(String name) {
maybeInit();
// We support directories so we can run tests and/or legacy code
// that uses Class.getResource.
if (isDirectory) {
File resourceFile = new File(dir, name);
if (resourceFile.exists()) {
try {
return resourceFile.toURI().toURL();
} catch (MalformedURLException ex) {
throw new RuntimeException(ex);
}
}
}
if (urlHandler == null) {
/* This element has no zip/jar file.
*/
return null;
}
return urlHandler.getEntryUrlOrNull(name);
}
}
其中,它有一个非常重要的成员变量DexFile。
接着回到主流程,我们通过makeDexElements()方法得到了一个elements数组。那么,makeDexElements()方法具体干了什么呢?
private static Element[] makeElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions,
boolean ignoreDexFiles,
ClassLoader loader) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* 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[elementsPos++] = new Element(file, true, null, null);
} else if (file.isFile()) {
if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
zip = file;
if (!ignoreDexFiles) {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements[elementsPos++] = new Element(dir, false, zip, dex);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
它的作用是通过loadDexFile()方法把dex文件都加载出来,然后返回一个elements数组。
接着,我们来看DexPathList中最重要的方法findClass()。
public Class findClass(String name, List<Throwable> 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;
}
它就是通过遍历elements数组,拿到里面的每一个dex文件,通过DexFile的loadClassBinaryName()方法找到class字节码。通过查看DexFile源码,可以得知loadClassBinaryName()方法最终是调用的底层C++方法来load class。
至此,我们就完整地了解了ClassLoader加载class的具体实现。
###六. 总结
本文讲解了Android中几种ClassLoader的作用,并且从源码角度讲解了class的加载过程。在Android中加载class,其实最终是通过DexPathList的findClass来加载的。
另外,这里贴一下几个文件源码的查看地址。