聊一聊 "类加载器"

阅读本文你能收获到

  • 了解 Java/Android中类加载器及其工作流程
  • Android 开发过程中类加载器的应用场景

类加载器


类加载器是虚拟机运行时动态加载字节码文件的入口。注意这里的虚拟机并不特指 Java虚拟机 或 Android虚拟机。通常来说开发者写的代码经过“前端编译器”编译成字节码文件集(Java-jar, Android-dex等)之后交给虚拟机,而虚拟机加载这些字节码文件集就是通过类加载器完成。

应用程序包的字节码文件集是经过“前段编译器”精挑细选得到的,程序运行所需要的任意代码及资源都被打进程序包中。但实际上用户在使用程序时往往难以走遍程序所有逻辑,因此虚拟机只需按需加载当前用户场景所涉及到的代码。这便决定了类加载器的加载场景 “按需加载”。

应用程序包实际运行是依赖“运行环境”的。比如 Java 程序需要依赖 JRE 进行运行,而 Android 程序则依赖 Application Framework。当然这里讨论的仅仅是下面第一层,更下层的还有Kernel等不在讨论范围内。而这些环境运行时提供了运行程序库依赖的支持。如果应用程序包中使用了运行环境中的类,那运行时这部分类该如何加载? 同时,应用程序内的类又是如何加载的呢? 事实上,虚拟机对要加载的类进行“域”划分,不同“域”的加载委托给不同的类加载器进行加载,这种加载模式称为 “委托加载”。

委托加载

除了系统提供的类加载,开发者也可以自定义 ClassLoader,在整个应用程序加载流程中所有类加载器有严格的加载逻辑, 按照 ”按需加载,委托加载“ 的思想组合起来一起完成类加载,这种模型称为 ”双亲委派模型“。在了解 Java 和 Android 平台的类加载器之前,我们先看下委托加载的流程。整个 “委托加载” 的思想都体现在 Java.lang.ClassLoader, 两个平台的加载流程方向上是一致的。

ClassLoader 是一个抽象类, 没有抽象方法, 加载流程统一集中在 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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 2
c = parent.loadClass(name, false);
} else {
// 3
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 4
c = findClass(name);
}
}
return c;
}
  • 1处 通过 native (VMClassLoader实现)手段检测当前 ClassLoader 是否已经加载过该类, 如果加载过则直接返回。
  • 2处 当且仅当 ClassLoader 没有加载过类且父加载器存在时, 尝试调用 parent#loadClass 获取
  • 3处 当且仅当 ClassLoader 没有加载过类且父加载器不存在时, 尝试通过 native 手段从引导类加载器获取
  • 如果上述途径都没有获取到, 则 findClass 获取, 默认方法内抛出异常, 子类需要实现覆盖实现自己的逻辑。

活动图如下

图片名称

值得注意的是, loadClass 一个 protected 权限级别的方法, 这意味着 ”双亲委派模型“ 并不是唯一的加载模式而是系统建议我们使用的模式。

再来看看 Java 和 Android 平台提供哪些类加载器, 以 Android 加载器的内容重点展开。

Java 流派

Java系统主要提供了 3 种类加载器

  1. Bootstrap Classloader,启动类加载器,负责加载 \lib 目录下或者被 -Xbootclasspath参数所指定的路径种的,能被虚拟机识别的类库 (即所有 java.* 开头的类)。
  2. Extension Classloader,扩展类加载器,负责加载 \lib\ext 目录下或者被 java.ext.dirs 系统变量指定路径的类库(例如所有 javax.* 开头的类和存放在 JRE 的 ext 目录下的类)。
  3. Application Classloader,应用程序类加载器,负责加载用户类路径指定的类库,开发者可以直接使用这个类加载器(即我们自己写的 Java程序时新创建的类都是通过它来接在的)。

Android 流派

Android系统也提供了多种类加载器, 这些类加载器都由 java.lang.ClassLoader 继承而来。

ps: 本文所涉及到的源码均以 Andriod P 版本作为展开分析

以 Android 类加载为例子,这里画了类图方便直观预览。

图片名称

上述类图除了橙色的 DexPathList 类之外, 其他都是 Android 提供的类加载器。按照 Java 系统类加载器的划分, Android 类加载器大致也可以划分 3 种类加载器。

  1. 加载 Framework 层类, BootClassLoader 为该类型类加载器, 在 Android 系统启动的时候创建该实例, 当应用程序系统也需要用到 Framework 类是会传递该类加载器实例给应用层。
  2. 加载已经安装的 apk 中的类, PathClassLoader 为该类型类加载器。
  3. 加载jar/apk/dex,未安装过的 apk 中的类, DexClassLoaderPathClassLoader 为该类型类加载器。

这里可能和你平时在网上看到的结论是不一样的, PathClassLoader 也可以加载外部 dex 了? 肯定的啊。 看下面分析。

BaseDexClassLoader


Android 应用层的类加载逻辑基本围绕 BaseDexClassLoader 及子类开展的, 也是应用层开发热修复, 插件化技术中重要的技术基础。下面重点分析这部分内容, 在掌握这部分知识之前, 希望我们能达成一致的共识 “带着问题在源码中找答案”。

待解决的问题 :

  • BaseDexClassLoader的设计初衷是什么?
  • 如何加载 Dex 文件 ?
  • 子类的应用场景是什么 ?
  • 实际开发过程我们可以如何使用 ?

设计初衷

由于移动端架构及性能原因, Android 针对 class 文件进一步优化形成 dex 文件。 为了适配加载 dex 文件, BaseDexClassLoader 应运而生。 在BaseDexClassLoader.DexPathList 源码中可知, BaseDexClassLoader 加载 dex 实际上是委托 DexPathList 对象进行的。

如何加载 Dex 文件

BaseDexClassLoader 中有一个重要的成员变量 pathList, 是一个 DexPathList 类型对象。结合 BaseDexClassLoader.DexPathList源码及类图可以理解, DexPathList 托管处理了 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
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
63
//from BaseDexClassLoader.java 部分源码做过删减
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}

public BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexFiles, librarySearchPath);
this.sharedLibraryLoaders = null;
}

@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

if (sharedLibraryLoaders != null) {
for (ClassLoader loader : sharedLibraryLoaders) {
try {
return loader.loadClass(name);
} catch (ClassNotFoundException ignored) {
}
}
}
return pathList.findClass(name, suppressedExceptions);
}

@Override
protected URL findResource(String name) {
if (sharedLibraryLoaders != null) {
for (ClassLoader loader : sharedLibraryLoaders) {
URL url = loader.getResource(name);
if (url != null) {
return url;
}
}
}
return pathList.findResource(name);
}

@Override
protected Enumeration<URL> findResources(String name) {
Enumeration<URL> myResources = pathList.findResources(name);
if (sharedLibraryLoaders == null) {
return myResources;
}
Enumeration<URL>[] tmp =
(Enumeration<URL>[]) new Enumeration<?>[sharedLibraryLoaders.length + 1];
for (int i = 0; i < sharedLibraryLoaders.length; i++) {
try {
tmp[i] = sharedLibraryLoaders[i].getResources(name);
} catch (IOException e) {
// Ignore.
}
}
tmp[sharedLibraryLoaders.length] = myResources;
return new CompoundEnumeration<>(tmp);
}

在创建 BaseDexClassLoader 对象的同时也创建了 DexPathList 对象。 sharedLibraryLoaders 是共享的 Loader 数组, 在查找 Resource 和 Class 是优先从 sharedLibraryLoaders 中获取, 这些 Loader 也是调用各自的查找方法, 最终会依赖 DexPathList 对象进行查找。

在熟悉 DexPathList 之前看下其构造器。

1
2
3
4
5
6
7
8
9
private Element[] dexElements;

public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles,String librarySearchPath) {
//...
}

DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
//...
}
  • definingContext 为关联的 ClassLoader
  • dexFiles 内存中已经存在的 dex 缓存
  • librarySearchPath native 库的路径
  • optimizedDirectory 存放优化过的 dex 文件

dexElements 是存放加载过的 dex 或 resource 资源。

下面重点看下 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
27
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;
}

static class Element {
/**
* A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
* (only when dexFile is null).
*/
@UnsupportedAppUsage
private final File path;
/** Whether {@code path.isDirectory()}, or {@code null} if {@code path == null}. */
private final Boolean pathIsDirectory;
@UnsupportedAppUsage
private final DexFile dexFile;
private ClassPathURLStreamHandler urlHandler;
private boolean initialized;
}

Element是一个静态类, 封装了一个 Dex 单元. 而 findClassdexElements 数组中获取, 所以追踪 dexElements 初始化。

makeInMemoryDexElements 方法直接读取 dexFiles(ByteBuffer[]), 类似于写文件。makeDexElements 则是从特定 path 下读取 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
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
63
64
65
66
67
68
69
70
71
72
73
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;

for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;

// 以 .dex 结尾
if (name.endsWith(DEX_SUFFIX)) {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException 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;
}

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

private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}

从 dexPath 中读取文件数组之后, 无论是否以 .dex 后缀结尾的文件, 都通过 loadDexFile 方法进行读取。唯一不同的地方在于, 当 optimizedDirectory 不为 null 时则通过 DexFile.loadDex 加载, 否则直接 new 一个 DexFile 对象。

optimizedPathFor 是用于转换文件后缀名的。 如果文件没有带.标识符则默认拼接 .dex 后缀, 否则则改写后缀为 .dex。 这样做得意义在于虚拟机能通过后缀名识别包含 dex 的文件提高读取效率。

DexFile.java 是如何加载 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
28
29
30
31
32
33
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
DexPathList.Element[] elements) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
mInternalCookie = mCookie;
mFileName = sourceName;
}

private static Object openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements) throws IOException {
// Use absolute paths to enable the use of relative paths when testing on host.
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null)
? null
: new File(outputName).getAbsolutePath(),
flags,
loader,
elements);
}

private static native Object openDexFileNative(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements);

DexPathList#loadDexFile 最终都是调用的是 DexFile 构造器。optimizedDirectory 不为 null 的场景下:outputName 为 null 且 flags 为 0。 openDexFileNative 是一个 native 方法。

nativa 方法如何追踪呢, 下载 aosp源代码之后, 使用 vs 进行预览。 DexFile 的包名为 “dalvik.system”, 则 native 文件名为 “dalvik_sytem_DexFile” 搜索一下就定位到了。 其中 DexFile_openDexFileNative 方法就是对应的 native 方法。

native 代码在虚拟机内部。dalvik_system_DexFile.cc 为官方源码。有兴趣可以 dowm 下来看看。

下面为整个 dex 加载流程的时序图.

图片名称

子类应用场景

BaseDexClassLoader的众多子类并没有覆盖其任何方法, 唯一不同的地方在于每个子类的构造器调用 super 参数不一致。

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
//from PathClassLoader.java
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}

//from DexClassLoader.java
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}

//from InMemoryDexClassLoader
public InMemoryDexClassLoader(@NonNull ByteBuffer @NonNull [] dexBuffers,
@Nullable String librarySearchPath, @Nullable ClassLoader parent) {
super(dexBuffers, librarySearchPath, parent);
}
public InMemoryDexClassLoader(@NonNull ByteBuffer @NonNull [] dexBuffers,
@Nullable ClassLoader parent) {
this(dexBuffers, null, parent);
}
public InMemoryDexClassLoader(@NonNull ByteBuffer dexBuffer, @Nullable ClassLoader parent) {
this(new ByteBuffer[] { dexBuffer }, parent);
}

可以确定的是 InMemoryDexClassLoader 不从 dexPath 路径中加载 dex 文件而是从 dex 缓存内容中读取。 但是 DexClassLoaderPathClassLoader 存在一样的构造器啊。翻开 BaseDexClassLoader 文档一开

1
optimizedDirectory	File: this parameter is deprecated and has no effect since API level 26.

optimizedDirectory 在 8.0 版本废弃会有什么影响呢 ? optimizedDirectory 是存放被优化过的 dex。在 dex 文件首次被加载的时候, 虚拟机会执行 dexopt 操作, 而 optimizedDirectory 就是优化后的odex文件的存放目录。在 native 加载 dex过程中, 如果 outputName 参数为 null 时则默认把优化后的的dex保存在为 `/data/dalvik-cache/xxx@classes.dex。 而PathClassLoader默认 *outputName* 参数为 null, 则 app 启动加载dex生成的优化文件正是存放在/data/dalvik-cache/xxx@classes.dex。8.0 之前 DexClassLoader 可以指定生成 odex 文件存放的目录,而 8.0 之后则不可以了, 默认都存在/data/dalvik-cache/xxx@classes.dex`。 从官网最新的文档上可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//from PathClassLoader.java
* 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).

//from DexClassLoader.java
* 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
* 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

就是为了保证新版本DexClassLoader 加载生成的 odex 文件不被随便存放, 其目的是为了避免加载的内容会对应用造成攻击。

这里放一下 app 默认的 PathClassLoader 加载流程:

  1. ActivityThread#handleBindApplication
  2. ContextImpl#getClassLoader
  3. LoadedApk#makePaths(计算dexPath) -> LoadedApk#getClassLoader
  4. ApplicationLoaders#getClassLoader
  5. PathClassLoaderFactory#createClassLoader
  6. PathClassLoader#构造器

有兴趣的朋友可以翻阅源码查看。

在 api 27 (Android 8.1) 中新增 DelegateLastClassLoader, 该类继承 PathClassLoader 。在 委托加载 章节中最后提到, “双亲委派模型”的加载并不是唯一的方式, 而 DelegateLastClassLoader 就是最好的证明。

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
//from DelagateLastClassLoader.java

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

// 1
Class<?> cl = findLoadedClass(name);
if (cl != null) {
return cl;
}

// 2
try {
return Object.class.getClassLoader().loadClass(name);
} catch (ClassNotFoundException ignored) {
}

// 3
ClassNotFoundException fromSuper = null;
try {
return findClass(name);
} catch (ClassNotFoundException ex) {
fromSuper = ex;
}

// 4
try {
return getParent().loadClass(name);
} catch (ClassNotFoundException cnfe) {
throw fromSuper;
}
}
  • 1处 通过 native (VMClassLoader实现)手段检测当前 ClassLoader 是否已经加载过该类, 如果加载过则直接返回。
  • 2处 尝试通过 native 手段从引导类加载器获取
  • 3处 则 findClass 获取, 默认方法内抛出异常, 子类需要实现覆盖实现自己的逻辑
  • 4处 则尝试调用 parent#loadClass 获取

实际开发的应用

插桩实现

广为人知的 Qzone 补丁方案就是通过插桩实现热修复, 下面为伪代码(8.0版本之前), 略去反射实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1
String dexPath = "补丁路径";
String dexoptPath = "xxxx/dexopt/";
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, dexoptPath, dexoptPath, getClassLoader());
DexPathList newPathList = dexClassLoader.pathList ;
Element[] newElements = newPathList.elements;

// 2
DexPathList currentPathList = getBaseDexClassLoader().pathList;
Element[] currentElements = pathList.elements;

// 3
Element[] resultElements = mergeElement(newElements, currentElements);
pathList.elements = resultElements;
  • 1处 加载补丁 dex
  • 2处 反射获取当前 dex 数组
  • 3处 把补丁 dex 查到 当前 dex 数组前面

这样一来, 在 DexPathList#findClass 过程中, 新插入的 element 元素就会优先被遍历到使用。

但是, 如果 currentElements 中某个 dex 中的类 A 持有 newElements 中某个 dex 中的类 B 的引用, 运行时会出现 Class ref in pre-verified clas resolved to unexpected implementation 。了解原因前得清楚虚拟机在 native 加载类的时候大致会做哪些工作。

  1. dexopt 会进行类校验 。比如校验 A 中 “static 方法,private 方法,构造函数,虚函数第一层引用 (c++)” 是否存在对某些类的引用且他们和 A 是在同个dex。 如果是则 A 会打上 “CLASS_ISPREVERIFIED” 。

  2. 虚拟机加载类的顺序为: dvmResolveClass -> dvmLinkClass -> dvmInitClasss 。 在 dvmResolveClass 会校验如果类被打上 CLASS_ISPREVERIFIED,则需要检验其引用类是否是同一个 dex ,不在同个 dex 则抛出 Exception。

既然如此, 那么就在 A 的 “static 方法,private 方法,构造函数,虚函数第一层引用 (c++)” 中任意一处引用另外一个 dex 的某个类来防止 A 被打上 “CLASS_ISPREVERIFIED”。 可以借助 gradle 入侵 dex 打包流程, 利用字节码技术对所有类的构造器插入对该 dex 中某个类的引用进而解决问题。

tinker Android N 类加载

Android_N混合编译与对热补丁影响解析 中一文已经明确指出 “无论是使用插入pathlist还是parent classloader的方式,若补丁修改的class已经存在与app image,它们都是无法通过热补丁更新的。它们在启动app时已经加入到PathClassLoader的ClassTable中,系统在查找类时会直接使用base.apk中的class。”

为了解决这种问题, tinker 团队选择在 Android N 及以上版本采用 “运行时替换PathClassLoader方案” 以达到废除 cache 效果。

SystemClassLoaderAdder#installDexes 中针对版本构建不同的 ClassLoader 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
throws Throwable {
// ...
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
classLoader = AndroidNClassLoader.inject(loader, application);
}
}
//...
}

public static AndroidNClassLoader inject(PathClassLoader originClassLoader, Application application) throws Exception {
AndroidNClassLoader classLoader = createAndroidNClassLoader(originClassLoader, application);
reflectPackageInfoClassloader(application, classLoader);
return classLoader;
}

在 Android N 上选择 AndroidNClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static AndroidNClassLoader createAndroidNClassLoader(PathClassLoader originalClassLoader, Application application) throws Exception {
// 1
final AndroidNClassLoader androidNClassLoader = new AndroidNClassLoader("", originalClassLoader, application);

// 2
final Field pathListField = ShareReflectUtil.findField(originalClassLoader, "pathList");
final Object originPathList = pathListField.get(originalClassLoader);

// 3
Object newPathList = recreateDexPathList(originPathList, androidNClassLoader, false);

// 4
pathListField.set(androidNClassLoader, newPathList);

// 5
ShareReflectUtil.findField(originPathList, "definingContext").set(originPathList, androidNClassLoader);

oldDexPathListHolder = originPathList;

return androidNClassLoader;
}
  • 1处 创建 androidNClassLoader 对象
  • 2处 反射获取原来的 originPathList 对象
  • 3处 通过反射取出 originPathList 对象的各个属性信息并反射构建一个新的 DexPathList 对象
  • 4处 DexPathList 对象关联 androidNClassLoader
  • 5处 androidNClassLoader 持有 DexPathList 对象

而在 findClass 过程中针对 application, tinker 库内的 loader 及一些特殊库文件, 默认使用 PathClassLoader 加载, 其他的优先使用 androidNClassLoader 查找。

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
public Class<?> findClass(String name) throws ClassNotFoundException {
if (applicationClassName != null && applicationClassName.equals(name)) {
return originClassLoader.loadClass(name);
} else if (name != null && name.startsWith("com.tencent.tinker.loader.")
&& !name.equals(SystemClassLoaderAdder.CHECK_DEX_CLASS)) {
return originClassLoader.loadClass(name);
} else if (name != null && name.startsWith("org.apache.http.")) {
// Here's the whole story:
// Some app use apache wrapper library to access Apache utilities. Classes in apache wrapper
// library may be conflict with those preloaded in BootClassLoader.
// So with the build option:
// useLibrary 'org.apache.http.legacy'
// appears, the Android Framework will inject a jar called 'org.apache.http.legacy.boot.jar'
// in front of the path of user's apk. After that, PathList in app's PathClassLoader should
// look like this:
// ["/system/framework/org.apache.http.legacy.boot.jar", "path-to-user-apk", "path-to-other-preload-jar"]
// When app runs to the code refer to Apache classes, the referred classes in the first
// jar override those in user's app, which avoids any conflicts and crashes.
//
// When it comes to Tinker, to block the cached instances in class table of app's
// PathClassLoader we use this AndroidNClassLoader to replace the original PathClassLoader.
// At the beginning it's fine to imitate system's behavior and construct the PathList in AndroidNClassLoader
// like below:
// ["/system/framework/org.apache.http.legacy.boot.jar", "path-to-new-dexes", "path-to-other-preload-jar"]
// However, the ART VM of Android P adds a new feature that checks whether the inlined class is loaded by the same
// ClassLoader that loads the callsite's class. If any Apache classes is inlined in old dex(oat), after we replacing
// the App's ClassLoader we will receive an assert since the Apache classes is loaded by another ClassLoader now.
return originClassLoader.loadClass(name);
}
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
// Some jars/apks other than base.apk was removed from AndroidNClassloader's dex path list.
// So if target class cannot be found in AndroidNClassloader, we should fallback to try
// original PathClassLoader for compatibility.
// Obviously this behavior violates the Parent Delegate Model, but it doesn't matter.
return originClassLoader.loadClass(name);
}
}

鉴于自己能力有限,如果本文中有遗漏或者错误的地方,请在评论区指出或者通过 yummyl.lau@gmail.com 邮件联系我,感谢。