(拉钩)Android工程师进阶34讲-05:深入理解ClassLoader的加载机制

0. 前言

第3节介绍了Java字节码文件(.class)的格式。一个完整的Java程序是由多个.class文件组成的,在程序运行过程中,需要将这些.class文件加载到JVM中才可以使用。而负责加载这些.class文件的就是类加载器(ClassLoader)。

1. Java中的类何时被加载器加载

在Java程序启动的时候,并不会一次性加载程序中的所有.class文件,而是在程序的运行过程中,动态地加载相应的类到内存中。

通常情况下,Java程序中的.class文件会在以下2种情况下被ClassLoader主动加载到内存中:

  • 1、调用类构造器
  • 2、调用类中的静态(static)变量或静态方法

2. Java中的ClassLoader

JVM中自带3个类加载器:

  • 1、启动类加载器BootstrapClassLoader
  • 2、扩展类加载器ExtClassLoader(JDK 1.9之后,改名为PlatformClassLoader
  • 3、系统加载器AppClassLoader

以上三者在JVM中各有分工,但是又相互依赖。

2.1 AppClassLoader系统类加载器

部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});

......

}

可以看出,AppClassLoader主要加载系统属性"java.class.path"配置下的文件,也就是环境变量CLASS_PATH配置的路径。因此AppClassLoader是面向用户的类加载器,编写的代码以及使用的第三方jar包通常都是由它来加载的。

2.2 ExtClassLoader扩展类加载器

部分源码:

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
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;

public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}

return instance;
}

......

public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}

private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];

for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}

return var1;
}

......

}

ExtClassLoader加载系统属性"java.ext.dirs"配置下类文件,可以打印出这个属性查看具体有哪些文件:

1
System.out.println(System.getPreperty("java/ext.dirs"));

结果如下:

1
/Library/Java/JavaVirtualMachines/jdk1.8.0_1051.jdk/Contents/Home/jre/lib/ext

2.3 BootstrapClassLoader启动类加载器

BootstrapClassLoader和上面两种ClassLoader不一样。

首先,它并不是使用Java实现的, 而是由C/C++编写的,它本身属于虚拟机的一部分。因此无法在Java代码中直接获取它的引用。如果在Java层获取BootstrapClassLoader的引用,系统会返回null

BootstrapClassLoader加载系统属性"sun.boot.class.path"配置下的类文件,可以打印出这个属性来查看具体有哪些文件:

1
System.out.println(System.getPreperty("sun.boot.class.path"));

结果如下:

可以看到,这些全是JRE目录下的jar包或.class文件。

3. 双亲委派模式(Parents Delegation Model)

JVM中已经有了三种ClassLoader,那么JVM又是如何知道该使用哪一种类加载器去加载相应的类呢?是双亲委派模式

3.1 双亲委派模式

双亲委派模式就是当类加载器接收加载类或资源的请求时,通常都是先委托给父加载器加载,只有当父加载器找不到指定类或资源时,自身才会执行实际的类加载过程。

其具体实现代码就是在ClassLoader.java中的loadClass()方法中,如下:java/lang/ClassLoader.java

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{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name); // 1
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false); // 2
} else {
c = findBootstrapClassOrNull(name); // 3
}
} 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.
c = findClass(name); // 4
}
}
return c;
}

说明:

  • 1、判断该Class是否已加载,如果已经加载,则直接将该Class返回。
  • 2、如果该Class没有被加载过,则判断parent是否为空,如果不为空,则将加载任务委托给parent
  • 3、如果parent = null,则直接调用BootstrapClassLoader加载该类。
  • 4、如果parentBootstrapClassLoader都没有加载成功,则调用当前ClassLoaderfindClass()方法继续尝试加载。

这个parent是什么呢?可以看ClassLoader的构造器:

1
2
3
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

可以看到,在每一个ClassLoader中都有一个ClassLoader类型的parent引用,并且在构造器中传入值。如果继续看源码,可以看到AppClassLoader传入的parent就是ExtClassLoader,而ExtClassLoader并没有传入任何parent,也就是null

3.2 举例

比如执行下面代码:

1
Test test = new Test();

默认情况下,JVM首先使用AppClassLoader去加载Test类。

  • 1、AppClassLoader将加载的任务委派给它的父类加载器——ExtClassLoader
  • 2、ExtClassLoaderparentnull,所以直接将加载任务委派给BootstrapClassLoader
  • 3、BootstrapClassLoader在jdk/lib目录下无法找到Test类,因此返回的Classnull
  • 4、因为parentBootstrapClassLoader都没有成功加载Test类,所以AppClassLoader会调用findClass()来加载Test

最终Test就是被AppClassLoader加载到内存中,可以通过如下代码验证此结果:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
ClassLoader cl = Test.class.getClassLoader();
System.out.println("cl is " + cl);

ClassLoader parent = cl.getParent();
System.out.println("parent is " + parent);

ClassLoader boot_strap = parent.getParent();
System.out.println("boot_strap is " + boot_strap);
}
}

打印结果为:

1
2
3
cl is sun.misc.Launcher$AppClassLoader@18b4aac2
parent is sun.misc.Launcher$ExtClassLoader@1b6d3586
boot_strap is null

注意:“双亲委派模式”只是Java推荐的模式,并不是强制的。我们可以继承ClassLoader,实现自己的类加载器。如果想保持双亲委派模式,应该重写findClass(name)方法;如果想破坏双亲委派模式,可以重写loadClass(name)方法。

4. 自定义ClassLoader

JVM中预置了三种ClassLoader,只能加载特定目录下的.class文件,如果想加载其他特殊位置下的jar包或类时(比如,要加载网络或磁盘上的一个.class文件),默认的ClassLoader就不能满足需求,所以需要自定义ClassLoader来加载特定目录下的.class文件。

4.1 自定义ClassLoader步骤

  • 1、自定义一个类继承抽象类ClassLoader
  • 2、重写findClass()方法。
  • 3、在findClass()中,调用defineClass()方法将字节码转换成Class对象,并返回。

伪代码:

1
2
3
4
5
6
7
8
public class 自定义ClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 寻找字节码
byte[] code = findCodeFromSomeWhere(name);
return this.defineClass(name, code);
}
}

4.2 自定义ClassLoader示例

首先创建测试类Secret.java

1
2
3
4
5
public class Secret {
public void printSecret() {
System.out.println("打印了 printSecret");
}
}

测试类所在的磁盘路径如下:

1
/Project/MyStudyDemo/LGDemo/src/Secret.java

下面创建DiskClassLoader继承ClassLoader,重写findClass()方法,并在其中调用defineClass()创建Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DiskClassLoader extends ClassLoader {
private String filePath;

public DiskClassLoader(String path) {
this.filePath = path;
}

// 重写findClass
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String newPath = filePath + name + ".class";
byte[] classBytes = null;
Path path = null;
try {
path = Paths.get(new URI(newPath));
classBytes = Files.readAllBytes(path);
} catch (URISyntaxException | IOException e) {
e.printStackTrace();
}
// 调用defineClass,创建Class并返回
return defineClass(name, classBytes, 0, classBytes.length);
}
}

最后,写一个测试自定义DiskClassLoader的测试类,验证自定义的DiskClassLoader是否正常工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DiskClassLoaderTest {
public static void main(String[] args) {
testClassLoader();
}

public static void testClassLoader() {
// 创建自定义ClassLoader对象
DiskClassLoader diskClassLoader = new DiskClassLoader("file:///Project/MyStudyDemo/LGDemo/src/"); // 1
try {
Class c = diskClassLoader.loadClass("Secret"); // 2
if (c != null) {
Object obj = c.newInstance();
// 通过反射调用Secret的printSecret方法
Method method = c.getDeclaredMethod("printSecret", null); // 3
method.invoke(obj, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

说明:

  • 1、代表需要动态加载的class路径。
  • 2、代表需要动态加载的类名。
  • 3、代表需要动态调用的方法名称。

最后执行,并打印如下结果,表示自定义的ClassLoader可以正常工作。

1
2
3
4
D:\Java\jdk1.8.0_212\bin\java.exe "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2020.1.2\lib\idea_rt.jar=55145:D:\Program Files\JetBrains\IntelliJ IDEA 2020.1.2\bin" -Dfile.encoding=UTF-8 -classpath D:\Java\jdk1.8.0_212\jre\lib\charsets.jar;D:\Java\jdk1.8.0_212\jre\lib\deploy.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_212\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8.0_212\jre\lib\javaws.jar;D:\Java\jdk1.8.0_212\jre\lib\jce.jar;D:\Java\jdk1.8.0_212\jre\lib\jfr.jar;D:\Java\jdk1.8.0_212\jre\lib\jfxswt.jar;D:\Java\jdk1.8.0_212\jre\lib\jsse.jar;D:\Java\jdk1.8.0_212\jre\lib\management-agent.jar;D:\Java\jdk1.8.0_212\jre\lib\plugin.jar;D:\Java\jdk1.8.0_212\jre\lib\resources.jar;D:\Java\jdk1.8.0_212\jre\lib\rt.jar;E:\Project\MyStudyDemo\LGDemo\out\production\LGDemo DiskClassLoaderTest
打印了 printSecret

Process finished with exit code 0

注意:上述动态加载.class文件的思路,经常被用作热修复和插件化开发的框架中,包括QQ空间热修复方案、微信Tink等原理都是由此而来。客户端只要从服务端下载一个加密的.class文件,然后在本地通过事先定义好的加密方式进行解密,最后再使用自定义ClassLoader动态加载解密后的.class文件,并动态调用相应的方法。

5. Android中的ClassLoader

本质上,Andorid系统和JVM是一样的,也需要通过ClassLoader将目标类加载到内存,类加载器之间也符合双亲委派模式。但是在Android中,ClassLoader的加载细节有略微的差别。

在Android虚拟机中是无法直接运行.class文件的,Android会将所有的.class文件转换成一个.dex文件,并且Android将加载.dex文件的实现封装在BaseDexClassLoader中,一般只是用它的两个子类:PathClassLoaderDexClassLoader

5.1 PathClassLoader

PathClassLoader用来加载系统apk和被安装到手机中的apk内的dex文件。它的2个构造函数如下:

1
2
3
4
5
6
7
8
9
10
11
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}

public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}

参数说明:

  • dexPath:dex文件路径,或者包含dex文件的jar包路径;
  • librarySearchPath:C/C++ native库的路径。

PathClassLoader里面除了这2个构造方法以外就没有其他方法了,具体的实现都是在BaseDexClassLoader里面,其dexPath比较受限制,一般是已经安装应用的apk文件路径。

当一个App被安装手机后,apk里面的class.dex中的class均通过PathClassLoader来加载的,可以通过如下代码验证:

1
2
3
4
5
6
7
8
9
10
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ClassLoader loader = MainActivity.class.getClassLoader();
Log.i("TAG", loader.toString());
}
}

打印如下:

1
2
System.out: dalvik.system.PathClassLoader
[DexPathList[[zip file "/data/app/com.ly.lgdemoandroid-1/base.apk"],nativeLibraryDirectories=[/data/app/com.ly.lgdemoandroid-1/lib/x86, /system/lib, /vendor/lib]]]

5.2 DexClassLoader

官方描述:

A class loader that loads classes from .jar and .apk filescontaining a classes.dex entry.

This can be used to execute code notinstalled as part of an application.

对比PathClassLoader只能加载已经安装应用的dex或apk文件,DexClassLoader则没有此限制,可以从SD卡上加载包含class.dex的.jar和.apk文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的dex的加载。

DexClassLoader的源码只有一个构造方法,如下:

1
2
3
4
5
6
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}

参数说明:

  • dexPath:包含class.dex的apk、jar文件路径,多个路径用文件分隔符(默认是“:”)分隔。
  • optimizedDirectory:用来缓存优化的dex文件的路径,即从apk或jar文件中提取出来的dex文件。该路径不可为空,且应该是应用私有的,有读写权限的路径。

5.2.1 使用DexClassLoader实现热修复

创建Android项目DexClassLoaderHotFix

项目结构:

ISay是一个接口,内部定义了一个方法:

1
2
3
public interface ISay {
String saySomething();
}

SayException实现了ISay接口,但是在saySomething()方法中,打印"something wrong here"来模拟线上的bug。

1
2
3
4
5
6
public class SayException implements ISay {
@Override
public String saySomething() {
return "something wrong here";
}
}

最后在MainActivity中,当点击按钮时,将saySomething返回的内容通过Toast显示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity {

private ISay say;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

say = new SayException();
Button btnSay = findViewById(R.id.btn_say);
btnSay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("TAG", "say_hotfix.jar not exists");
Toast.makeText(MainActivity.this, say.saySomething(), Toast.LENGTH_SHORT).show();
}
});
}
}

点击按钮:

创建HotFix patch包

新建Java项目,并分别创建两个文件ISay.javaSayHotFix.java

1
2
3
4
5
package com.ly.dexclassloaderhotfix;

public interface ISay {
String saySomething();
}
1
2
3
4
5
6
7
8
package com.ly.dexclassloaderhotfix;

public class SayHotFix implements ISay {
@Override
public String saySomething() {
return "Everything is right!";
}
}

ISay接口的包名和类名必须和Android项目中的一样。SayHotFix实现ISay接口,并在saySomething()中返回新的结果,用来模拟修复bug。

ISaySayHotFix打包成say_something.jar,然后通过dx工具将生成的say_something.jar包中的class文件优化为dex文件。

dx –dex –output=say_something_hotfix.jar say_something.jar

上述say_something.jar就是最终需要用作hotfix的jar包。

将HotFix patch 包拷贝到SD卡主目录中,并使用DexClassLoader加载SD卡中的ISay接口

首先将HotFix patch保存到本地目录下。一般在真实项目中,可以通过向后端发送请求的方式,将最新的HoFix patch下载到本地中。这里为了演示,直接使用adb命令将say_something.jar包push到SD卡目录下:

adb push say_something_hotfix.jar /storage/self/primary/

下面,修改MainActivity中的逻辑,使用DexClassLoader加载HotFix patch中的SayHotFix类,如下:

6. 总结

  • ClassLoader就是用来加载class文件的,不管是jar中还是dex中的class。
  • Java中的ClassLoader通过双亲委托模式来加载各自指定路径下的class文件。
  • 可以自定义ClassLoader,一般覆盖findClass()方法,不建议重写loadClass()方法。
  • Andorid中常用的两种ClassLoader分别为:PathClassLoaderDexClassLoader
  • Copyrights © 2019-2020 Tyler Liu

请我喝杯咖啡吧~

支付宝
微信