0%

Android 类加载器机制

  • 在 Java 中 .java 源文件会先编译成 .classs 字节码文件,然后借由 JVM 虚拟机将 .class 字节码加载进内存,最终程序得到运行。其中.class 加载过程就是由 类加载器(ClassLoader) 来完成的。
  • 而 Android 与 Java 相似, .java 源文件会先编译成 .dex 文件(.class 的集合),然后 Android 虚拟机( ART虚拟机 和 Dalvik虚拟机)将 .dex 文件加载进内存,最终程序得到运行。dex 加载过程也是由 类加载器(ClassLoader)完成的。

什么是类加载?

凭借一个类的全限定名得到对应的可以描述该类的二进制字节流,并将这些字节流转化为方法区的某种数据结构,生成一个 java.lang.Class 对象作为方法区这个类各种数据访问入口。这个过程就是类加载。

类加载过程就是由 ClassLoader 来完成的。

类加载器

每个类加载器都有独立的类名称空间。不同类型的类加载器加载同一个 .class 文件得到的类其实是不一样的。只有被同样的加载器加载加载的情况下,对类的比较才有意义(即 Class 对象的 equals, isAssignableFrom(), isInstance() 以及 instance 关键字所对应的函数或者表达式的返回结果是否有效)

Java中的类加载器

Java 中的类加载器分为三种:

  • Bootstrap ClassLoader(启动类加载器),加载 Java 中的核心类,在 JVM 中由 C++ 实现,其他的类加载器都是在 Java 层的实现。负责将 %JAVA_HOME\lib% 目录下 和 -Xbootclasspath 参数指定目录下的类加载到 JVM 中
  • Extension ClassLoader(扩展类加载器),加载 %JAVA_HOME%/lib/extjava.ext.dirs 所指定的路径下的类到 JVM 中
  • Application ClassLoader(应用类加载器),加载 %CLASSPATH% 路径下的类。包含了自定义的 ClassLoader

某个具体的类,该由哪个加载器加载呢,加载原则是什么呢?

其加载原则就是 双亲委派 原则。

注:这里的 双亲委派 其实翻译的不够精确,容易让人误解为有两个父类,实际上 Java 是只能单继承的。英文原文为 Parents Delegate 这里应该理解为 父亲委派 原则。

也就是说当某个类加载器要加载某个类时,先委托其父加载器加载,依次会传递至 Bootstrap ClassLoader。父加载器处理不了的,则由其子加载器来处理。

看一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 先检查这个类是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) { // 类没有被加载过
try {
if (parent != null) { // 委托父加载器加载
c = parent.loadClass(name, false);
} else { // 最顶端,由Bootstrap加载器来加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}

if (c == null) { // 父加载器都没能加载这个类,则交给这个 ClassLoader 本身来加载
c = findClass(name);
}
}
return c;
}
}

有了 双亲委派 原则,先会让上层类加载器加载,上层的类加载器优先级高。比如基础的类 java.lang.Object 无论是什么类加载器加载,都会传递给最顶层加载器去加载,最终得到的 Object 类都是同样的 Object 类,保证了一致性。如果没有 双亲委派 原则,那么各个类加载器加载的 Object 都不是同一个类,也就违背了前面提到的类加载器特点。

Android 中的类加载器

Android 虚拟机中加载的是 .dex 文件(多个 .class 合并而来),跟 Java 的类加载机制相似也不同。

Android ClassLoader 源码分析

在这里以 Android 9.0 为例进行类加载机制的源码分析,源码地址:/dalvik/system/

我们来看一下 Android 源码中对 PathClassLoaderDexClassLoader 的注释说明:

1
2
3
4
5
6
7
8
9
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
public class PathClassLoader extends BaseDexClassLoader {
// ...
}

PathClassLoader 用于加载系统类和应用本身的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* <p>Prior to API level 26, this class loader requires an
* application-private, writable directory to cache optimized classes.
* Use {@code Context.getCodeCacheDir()} to create such a directory:
* <pre> {@code
* File dexOutputDir = context.getCodeCacheDir();
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
// ...
}

DexClassLoader 可以加载包含了 .dex.jar 以及 .apk 中的类。DexClassLoader 可以用于加载 App 未被安装的那一部分的 dex。需要注意的是,不要把类优化缓存目录放在外部存储空间来避免注入攻击等不安全隐患。

Android 类加载器构造函数

ClassLoader 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public abstract class ClassLoader {

static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}

private final ClassLoader parent;

private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}

private static Void checkCreateClassLoader() {
return null;
}

private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}

protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
}

getSystemClassLoader() 得到的是一个 PathClassLoader,它的父类加载器是一个 BootClassLoader。也就是说,系统默认的类加载器是 PathClassLoader

PathClassLoader 构造函数

1
2
3
4
5
6
7
8
9
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);
}
}

PathClassLoader 构造函数很简单,直接调用父类 BaseDexClassLoader 的构造函数。第二个构造参数始终是 null,表示 optimizedDirectory 始终为 null

BaseDexClassLoader 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
/**
* Constructs an instance.
* Note that all the *.jar and *.apk files from {@code dexPath} might be
* first extracted in-memory before the code is loaded. This can be avoided
* by passing raw dex files (*.dex) in the {@code dexPath}.
*
* @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 this parameter is deprecated and has no effect since API level 26.
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
}
  • dexPath:包含 类或者 资源 的 .jar/.apk 路径,如果是多个路径,则用 File.pathSeparator(默认是 :) 来分隔。当然也可以直接传 dex 的路径。
  • optimizedDirectory:在 API 26(Android 8.0)的版本中,它表示 odex(optimized dex) 读写存放目录,如果传 null 则表示使用系统默认的目录来存储。自 Android 8.0 起,这个参数已经被弃用,不再生效,使用系统默认的目录。
  • librarySearchPath:native 库文件存放目录,多个库文件则用 File.pathSeparator(默认是 :) 分隔。
  • parent: 父类加载器
  • isTrusted: 当前加载的 dex 是否受信任,如果受信任则可以访问平台隐藏的API,默认为 false

BaseDexClassLoader 中有一个 DexPathList 类的 pathList 成员变量,它表示 dexPath 下的 .dex 列表。

DexPathList 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";

private Element[] dexElements;

NativeLibraryElement[] nativeLibraryPathElements;

public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}

DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
// ...

this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);

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);

// ...
}
}

Element 用来描述一个 dex 文件所代表的元素。字段 dexElements 则为 dex 文件元素列表,通过 makeDexElements() 方法来初始化。

NativeLibraryElement 用来描述一个库文件所代表的元素,字段 nativeLibraryPathElements 则为库文件元素列表,通过 makePathElements 方法来初始化。

makeDexEleemnts() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;

// 打开所有文件,加载文件中(包括原始的dex文件 或者 包含dex文件的压缩文件)所有的 dex 文件
for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// `.dex` 文件
try {
// 初始化表示 .dex 文件的 DexFile
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
// 包含 `.dex` 文件的压缩包(.jar/.zip 等)
try {
// 初始化表示 .dex 文件的 DexFile
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}

// 初始化表示 .dex 文件的 DexFile
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}

makeDexElements() 就是找到指定文件列表中的所有的 .dex 文件,以数组的形式返回。 loadDexFile 会初始化对应的 DexFile 类,DexFile 代表 .dex 文件。DexFile 在初始化过程中会打开对应的 .dex 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class DexFile {
private Object mCookie;
private Object mInternalCookie;
private final String mFileName;

DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
mCookie = openDexFile(fileName, null, 0, loader, elements);
mInternalCookie = mCookie;
mFileName = fileName;
}

// 打开 Dex 文件
private static Object openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null)
? null
: new File(outputName).getAbsolutePath(),
flags,
loader,
elements);
}

// native 方法: 打开 Dex 文件
private static native Object openDexFileNative(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements);
}

DexClassLoader 构造函数

1
2
3
4
5
6
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}

DexClassLoaderPathClassLoader 相似。和前面一样,第二个参数 optimizedDirectory 也从 Android 8.0 开始弃用,不再有效。

类加载器构造函数小结

  1. BaseDexClassLoader 类(PathClassLoaderDexClassLoader)在初始化过程中,会找到其相关的 .dex 列表进行初始化。
  2. 系统默认的类加载器就是 PathClassLoader

Android 类加载器加载类过程

类加载器加载类的过程,其实就是 ClassLoader 的方法 loadClass()

ClassLoader#loadClass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public abstract class ClassLoader {
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}

if (c == null) {
c = findClass(name);
}
}
return c;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}

我们之前分析过,类加载过程采用的遵循 双亲委派 原则。ClassLoader#findClass() 方法会直接抛出异常,说明 ClassLoader 的子类需要重写该方法才有意义。

BaseDexClassLoader#findClass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

@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;
}
}

BaseDexClassLoader 实现了 findClass 方法,实际上是在通过 DexPathList#findClass() 方法在 pathList 中找有没有指定的类。

DexPathList#findClass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*package*/ final class DexPathList {
private Element[] dexElements;

public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

/*package*/ static class Element {
private final File path;
private final DexFile dexFile;

public Class<?> findClass(String name, ClassLoader definingContext, List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
}
}

从上面源码中,可以发现,类加载是在 dexElements 数组中寻找对应的类,一旦在某一个 .dex 文件中找到指定的类,则不再继续查找,并直接返回。

DexFile#loadClassBinaryName()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class DexFile {
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}

private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)
}

最终会调用 native 层的 defineClassNative 方法,来查找 .dex 文件中相应的类。

Android 类加载器加载类过程小结

  • Android 类加载过程也遵循 双亲委派 原则
  • BaseDexClassLoader 加载类是顺序的,前面的 .dex 中如果能找到指定的类,则后面的不再查找

总结

  • Java 和 Android 类加载过程都遵循 双亲委派 原则
  • PathClassLoader 用于加载系统类和应用本身的类。系统默认的类加载器就是 PathClassLoader
  • DexClassLoader 可以用于加载 App 未被安装的那一部分的 dex
  • BaseDexClassLoader 加载类是顺序的。