Android 插件化与热更新技术日渐成熟,当你研究这些技术时会发现类加载器在其中占据重要地位。Java 语言天生就有灵活性、动态性,支持运行期间动态组装程序,而这一切的基础就是类加载器。
Java 源代码被编译器编译成字节码,即从. java 文件编译为. class 文件,而. class 文件就是通过类加载器加载到虚拟机内存中的。
虚拟机的类加载(Class Loading)过程分为加载、链接(验证、准备、解析)、初始化、使用、卸载等过程。这里仅考虑加载这个阶段,在此阶段虚拟机的工作有以下几点:
注意看第 1 条,虚拟机规范只是说来获取一个类的二进制字节流,但并没有说从哪里获取,怎样获取,这也就意味着 Class 文件可以来自磁盘、ZIP 文件、JAR 文件、数据库、甚至来自网络或者在程序运行时动态生成。上述各种来源的 Class 文件都是由类加载器(Class Loader)来加载的,也正因为如此,Java 才拥有高度的灵活性和动态性。
Java 中的类加载器至少有三种:
此外,用户还可以继承 ClassLoader 类来自定义类加载器,这样就可以在向虚拟机传递字节码之前进行需求定制了。
注意:对于任意一个 Java 类,它在虚拟机里的唯一性是由其类本身及其类加载器共同决定的。如果两个类来自同一个 Class 文件,在同一个虚拟机中,但是被不同的 ClassLoader 所加载,那么这两个类在虚拟机中也是不相等的。
先来看下 Java 中的类加载器层次关系:
上述层次关系称为类加载器的双亲委派模型,它是在 JDK 1.2 中引入的,其实它并非强制性的约束,而是推荐我们使用的一种类加载机制,可以看到除了顶部的启动类加载器之外,其他加载器都有一个父类加载器。
双亲委派模型的工作流程:当一个类收到加载类的请求时,它自己先不进行加载,而是把该请求委派为父类加载器去完成,父类加载器也是如此,直到将加载类的需求传给顶层的启动类加载器;只有当父类加载器无法完成加载时(在自己的搜索范围中没有找到该类),子加载器才尝试自己去完成类加载,如果加载不了,则会抛出 ClassNotFoundException 异常。
有一点需要注意:如果扩展类加载器收到请求去加载一个类,它会先委托启动类加载器去加载,如果启动类加载器加载不了,则尝试自己加载。如果扩展类加载器也无法加载,则直接抛出 ClassNotFoundException 异常而结束,并不会再交给下一层的应用类加载器去加载。
说明了双亲委派模型的原理后,再来看下其源码实现,代码逻辑很简单,也证实了上述讲到的双亲委派模型的工作流程:
- public Class loadClass(String name) throws ClassNotFoundException {
- return loadClass(name, false);
- }
- protected synchronized 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 {
- // 如果不存在父类加载器,就检查是否由启动类加载器加载
- // 通过调用native方法 findBootstrapClass0(String name)
- c = findBootstrapClass0(name);
- }
- } catch (ClassNotFoundException e) {
- // 如果父加载器和启动类加载器都不能完成加载任务,自身才尝试去加载
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
使用双亲委派模型来组织各种类加载器,使之遵循了一定的优先级层次,从而能保证 Java 运行环境的稳定与条理性。例如 java.lang.Object 类是所有类的基类,并且根据双亲委派模型它是由启动类加载器加载的,如果我们也自定义了一个 java.lang.Object 类(只是假如,其实虚拟机会对 java.lang 开头的自定义类抛异常)并放在应用程序的 ClassPath 中去加载,那么应用中就会出现多个 Object 类,从而会导致 Java 类型体系混乱而无法正常运行。
另一个好处是避免类的二次加载。从上述 loadClass 源码中可知,先判断该类是否被加载过,如果已被加载过则直接返回该类。当一个类加载器委托父类加载时也是执行此逻辑,从而保证某些类只被加载一次。
由于自定义类加载器通常继承 ClassLoader,来看下 ClassLoader 的几个主要方法:
- // 加载指定完整名称的二进制字节流,不建议子类加载器重写,否则可能会破坏双亲委派模型
- public Class loadClass(String name) throws ClassNotFoundException {…
- }
- // 加载指定完整名称的二进制字节流,不建议子类加载器重写,否则可能会破坏双亲委派模型
- protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {…
- }
- // 被loadClass方法调用去加载指定名称类,官方建议子类加载器重写该方法
- protected Class findClass(String name) throws ClassNotFoundException {…
- }
- // 该方法将二进制字节流转换为Class,一般在findClass方法中读取到对应字节码后调用,由于是final方法,故不可继承,其功能具体由虚拟机实现,Java层不需要关心
- protected final Class defineClass(String name, byte[] b, int off, int len) throws ClassFormatError {…
- }
上述几个方法的说明可参考注释。
为了遵循双亲委派模型,当自定义类加载器时,官方建议我们仅仅重写 findClass() 方法,而不要重写 loadClass() 方法,否则就有可能破坏双亲委派模型。当然前面也说了,双亲委派模型并非强制约束,如有特别需要,也可以自行确定类的加载规则。一个典型的自定义类加载器如下:
- public class CustomClassLoader extends ClassLoader {
- ……
- @Override
- protected Class findClass(String name) throws ClassNotFoundException {
- // 获取类的字节数组
- byte[] classData = getClassData(name);
- if (classData == null) {
- throw new ClassNotFoundException();
- } else {
- return defineClass(name, classData, 0, classData.length);
- }
- }
- ……
- }
由此可知类加载器的各个方法的执行顺序为:loadClass—>findClass—>defineClass。
Android 应用通常是使用 Java 来开发的,也是运行在虚拟机 Dalvik 或 ART 上。虽然 Android 的虚拟机跟标准的 Java 虚拟机是不同的,但是类的加载机制都是类似的,即理论上 Android 也可以像 Java 程序一样,灵活地动态加载,如今大量的 Android 插件化、热更新框架都利用了此技术。
在一个 Android 工程的 Application 中加入几行日志来打印下,代码如下:
- package com.aspook.androidnotes;
- import android.app.Application;
- import android.util.Log;
- public class MyApplication extends Application {
- @Override
- public void onCreate() {
- super.onCreate();
- ClassLoader loader = getClassLoader();
- if (loader != null) {
- Log.d("ABC", "classLoader :" + loader);
- while (loader.getParent() != null) {
- loader = loader.getParent();
- Log.d("ABC", "classLoader :" + loader);
- }
- }
- }
- }
在 Android Studio 中启动 App 后,依次输出 3 条 Log 如下:
这里出现了 3 种 ClassLoader,分别是:dalvik.system.PathClassLoader、com.android.tools.fd.runtime.IncrementalClassLoader、java.lang.BootClassLoader。第二个类加载器是用于 Instant Run 的,如果关闭 Android Studio 的 Instant Run 功能,再运行 App 则只会输出两种 ClassLoader。
通过查看 dalvik.system 包下的源码,发现还有一种 ClassLoader 叫做 DexClassLoader,稍后会介绍其用途。
其官方说明如下:
PathClassLoader 是 ClassLoader 的简单实现且只能加载本地的列表文件或目录,在 Android 中也就是已安装好的 APK,它不能加载来自网络的类。Android 中的系统类加载器与应用类加载器都是 PathClassLoader。
先来看其源码(7.0):
- package dalvik.system;
- import dalvik.system.BaseDexClassLoader;
- import java.io.File;
- 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 文件的 JAR/ZIP/APK 文件的路径
librarySearchPath:native library 文件的路径
parent:父类加载器
再来看 BaseDexClassLoader 的源码:
- package dalvik.system;
- import java.io.File;
- import java.net.URL;
- import java.util.Enumeration;
- public class BaseDexClassLoader extends ClassLoader {
- public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
- throw new RuntimeException("Stub!");
- }
- protected Class findClass(String name) throws ClassNotFoundException {
- throw new RuntimeException("Stub!");
- }
- protected URL findResource(String name) {
- throw new RuntimeException("Stub!");
- }
- protected Enumeration findResources(String name) {
- throw new RuntimeException("Stub!");
- }
- public String findLibrary(String name) {
- throw new RuntimeException("Stub!");
- }
- protected synchronized Package getPackage(String name) {
- throw new RuntimeException("Stub!");
- }
- public String toString() {
- throw new RuntimeException("Stub!");
- }
- }
BaseDexClassLoader 构造方法中有一个新的参数为 optimizedDirectory,它表示优化后的 dex 文件要写入的路径,此处可以为 null。
BaseDexClassLoader 继承自 java.lang.ClassLoader,它跟纯 Java 环境下的 java.lang.ClassLoader 还是有些不同的,虽然双亲委派的加载机制类似。
结合最初的 Log 输出可知,PathClassLoader 只能加载 "/data/app/com.aspook.androidnotes-2/base.apk" 中的类,也就是已安装到手机中的 APK,因此 PathClassLoader 作为默认的应用类加载器。
其官方说明如下:
DexClassLoader 可以从包含 dex 文件的 JAR 或 APK 中来加载类,而这些代码源允许不必是安装应用的一部分,因此可用于动态加载。
先来看下 DexClassLoader 的源码(7.0):
- package dalvik.system;
- import dalvik.system.BaseDexClassLoader;
- import java.io.File;
- 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!");
- }
- }
它同样继承自 BaseDexClassLoader,是 java.lang.ClassLoader 的子类,因此 DexClassLoader 与 PathClassLoader 都默认遵循双亲委派模型。
DexClassLoader 构造方法中的参数,我们前文都已经提及,注意的一点是 optimizedDirectory 参数在这里不能为 null。
与 PathClassLoader 不同,DexClassLoader 则打破了 PathClassLoader 的局限,它可以加载已安装应用之外的 APK、JAR 或 ZIP 中的 dex 文件,通常建议使用如下路径:
- File dexOutputDir = context.getCodeCacheDir();
不建议使用外部存储,因为外部存储没有提供足够的访问权限控制,容易引发代码注入攻击。
因此,Android 中实现动态插件通常是自定义继承自 DexClassLoader 的类加载器;如果插件为已安装的 APK,则可以使用 PathClassLoader。
BootClassLoader 直接继承自 java.lang.ClassLoader,其定义如下:
- class BootClassLoader extends ClassLoader {
- private static BootClassLoader instance;
- @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
- public static synchronized BootClassLoader getInstance() {
- if (instance == null) {
- instance = new BootClassLoader();
- }
- return instance;
- }
- public BootClassLoader() {
- super(null);
- }
- @Override
- protected Class findClass(String name) throws ClassNotFoundException {
- return Class.classForName(name, false, null);
- }
- @Override
- protected URL findResource(String name) {
- return VMClassLoader.getResource(name);
- }
- @SuppressWarnings("unused")
- @Override
- protected Enumeration findResources(String resName) throws IOException {
- return Collections.enumeration(VMClassLoader.getResources(resName));
- }
- @Override
- protected Package getPackage(String name) {
- if (name != null && !name.isEmpty()) {
- synchronized (this) {
- Package pack = super.getPackage(name);
- if (pack == null) {
- pack = definePackage(name, "Unknown", "0.0", "Unknown", "Unknown", "0.0",
- "Unknown", null);
- }
- return pack;
- }
- }
- return null;
- }
- @Override
- public URL getResource(String resName) {
- return findResource(resName);
- }
- @Override
- protected Class loadClass(String className, boolean resolve)
- throws ClassNotFoundException {
- Class clazz = findLoadedClass(className);
- if (clazz == null) {
- clazz = findClass(className);
- }
- return clazz;
- }
- @Override
- public Enumeration getResources(String resName) throws IOException {
- return findResources(resName);
- }
- }
通常在自定义类加载器时,都需要在构造方法中传入一个父加载器,而 BootClassLoader 的构造方法如下,没有传入 parent,而是传入一个 null:
- public BootClassLoader() {
- super(null);
- }
因此调用 BootClassLoader 的 getParent 方法时返回值为 null。
BootClassLoader 用来加载系统框架级别的类,例如 Context.class.getClassLoader() 与 ListView.class.getClassLoader() 的返回值类型均为 BootClassLoader。
当调用
这句代码时,会输出如下结果:
- ClassLoader.getSystemClassLoader()
发现系统类加载器也是 dalvik.system.PathClassLoader,与最初应用的类加载器(也是 dalvik.system.PathClassLoader)不同的是 DexPathList 的路径不同。
跟踪一下源码:
- public static ClassLoader getSystemClassLoader() {
- return SystemClassLoader.loader;
- }
- static private class SystemClassLoader {
- public static ClassLoader loader = ClassLoader.createSystemClassLoader();
- }
- /**
- * Encapsulates the set of parallel capable loader types.
- */
- private static ClassLoader createSystemClassLoader() {
- String classPath = System.getProperty("java.class.path", ".");
- String librarySearchPath = System.getProperty("java.library.path", "");
- // String[] paths = classPath.split(":");
- // URL[] urls = new URL[paths.length];
- // for (int i = 0; i < paths.length; i++) {
- // try {
- // urls[i] = new URL("file://" + paths[i]);
- // }
- // catch (Exception ex) {
- // ex.printStackTrace();
- // }
- // }
- //
- // return new java.net.URLClassLoader(urls, null);
- // TODO Make this a java.net.URLClassLoader once we have those?
- return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
- }
而 System.getProperty("java.class.path") 返回值为 ".",似乎可以解释系统类加载器的 DexPathList 的路径了。
与 Java 中类加载器的层次结构类似,具体如下图:
本文简单介绍了类加载器的基本概念,罗列了 Java 及 Android 中常用的类加载器,并对各种类加载器的特点及功能做了说明,另外对类加载器的双亲委派机制做了详细讲解,对于 Android 插件化及热更新技术则不在本文的讨论之内,后续会继续分享。
来源: http://www.bubuko.com/infodetail-1975117.html