Android 中的 ClassLoader

31 篇文章 0 订阅
23 篇文章 0 订阅

一、ClassLoader

ClassLoader 是用来加载 class 的类,它是一个抽象类。通过一个类的二进制名,一个 classLoader 会尝试去定位或生成组成一个类需要的数据。一个典型的策略是把类名转化为文件名,然后从文件系统读取这个文件名的 class 文件。

二进制名:

"java.lang.String"
"javax.swing.JSpinner$DefaultEditor"
"java.security.KeyStore$Builder$FileBuilder$1"
"java.net.URLClassLoader$3$1"

每个 class 对象都包含一个加载它的 classLoader 的引用。

ClassLoader 使用双亲委托模型去搜索类和资源。每个 ClassLoader 的实例都有一个相关联的父加载器。当需要去寻找一个类或资源,一个 ClassLoader 实例在自己去寻找这个类或资源之前,会先委托他的父加载器去寻找。java 虚拟机内置的类加载器叫引导类加载器(BootClassLoader),它没有父加载器,但是可以作为父加载器。

类加载器如果需要支持并发,需要在类初始化的时候通过激活 ClassLoader.registerAsParallelCapable 方法注册。(类加载器默认会注册为可并发)。但是,它的子类如果需要支持,仍然需要自己注册一遍。
在没有严格分级的委托模型环境中,类加载器必须是可并发的,否则类加载可能导致死锁,因为加载器在加载的过程中会拥有锁。

正常来说,java 虚拟机从本地文件系统加载类到独立平台。例如,在 UNIX 系统中,虚拟机从 CLASSPATH 的环境变量所标识的路径下加载类。

但是,一些类可能不是从文件生成的,他们可能从其他源生成,比如网络,或者它们可能通过一个应用来生成。 defineClass 方法将一个字节数组变成一个 class 类的实例,这个新的 class 的实例可以通过 Class.newInstance 方法来生成。

通过类加载器生成的对象的方法和构造器可以引用其他的类。为了决定这个类引用的地方,java 虚拟机激活一开始生成这个类的 ClassLoader 的 loadClass 方法。

例如,一个应用可以创建一个网络类加载器去从服务端下载 class 文件,示例代码:

ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();

这个网络类加载器必须定义 findClass 方法。一旦它下载了可以组成类的字节序列,它应该使用 defineClass 方法去生成这个类的实例。示例代码:

class NetworkClassLoader extends ClassLoader {
    String host;
    int port;

    public Class loadClass(String className, boolean resolve) {
        ...
        findClass(className);
        ...
    }
    public Class findClass(String className) {
        byte[] b = loadClassData(className);
        return defineClass(className, b, 0, b.length);
    }
    private byte[] loadClassData(String name) {
        // load the class data from the connection
        . . .
    }
}

1.1 Java 与 Android 的对比

java 中的 ClassLoader 和 Android 中的 ClassLoader 对应关系:
在这里插入图片描述
代码测试:

// 系统加载器
System.out.println(ClassLoader.getSystemClassLoader());
// 自定义类加载器
System.out.println(MyTest.class.getClassLoader());
// 自定义类加载器,的父加载器
System.out.println(MyTest.class.getClassLoader().getParent());
// 自定义类加载器,的父加载器,的父加载器
System.out.println(MyTest.class.getClassLoader().getParent().getParent());
// 系统类加载器
System.out.println(String.class.getClassLoader());

输出:

Java:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@7f31245a
null
null

Android:
dalvik.system.PathClassLoader[DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system/lib64]]]
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.gdeer.gdtesthub-YaOqSqpCvFgigrjg3u-rRg==/base.apk"],nativeLibraryDirectories=[/data/app/com.gdeer.gdtesthub-YaOqSqpCvFgigrjg3u-rRg==/lib/x86_64, /system/lib64]]]
java.lang.BootClassLoader@5de531
null
java.lang.BootClassLoader@5de531

二、Android 中的 ClassLoader

ClassLoader

2.1 BootClassLoader

Android 中默认无父构造函数传入的情况下,默认父构造器为一个 PathClassLoader 且此 PathClassLoader 父构造器为 BootClassLoader。因此,BootClassLoader 是作为最终父类的类构造器。
BootClassLoader 是 ClassLoader 的内部类,是包内可见,我们无法使用,也不能动态加载。

2.2 URLClassLoader

URLClassLoader 继承于 SecureClassLoader。URLClassLoader 只能用于加载 Jar 文件,但由于 dalvik 不能直接识别 jar,所以在 Android 中无法使用这个加载器。(那 android 中怎么加载 jar 的?)

2.3 BaseDexClassLoader

BaseDexClassLoader 用于加载各种 dex 中的类。 PathClassLoader、DexClassLoader 都只是在构造函数上对其简单封装而已。

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
    throw new RuntimeException("Stub!");
}

构造参数有四个:dexPath、optimizeDirectory、libraryPath、parent

dexPath:指目标类所在的 dex、apk 或 jar 文件的路径(可以是 sd 卡的路径)。如果要包含多个路径,路径之间必须使用特定的分隔符进行分割,特定的分割符可从 System.getProperty(“path.separtor”) 获得。最终会将 dexPath 路径上的文件 odex 优化到内部位置 optimizedDirectory,再进行加载。

optimizedDirectory:由于 dex 被包含在 apk 或 jar 文件中,因此在加载目标类之前需要先从 apk 或 jar 中解压出 dex 文件,该参数就是指定解压出的 dex 文件存放的路径。这也是对 apk 中 dex 根据平台进行 odex 优化的过程。apk 是一个程序压缩包,里面包含 dex 文件,odex 优化就是把包里的 dex 提取出来,变成 odex 文件,因为你提取出来了,系统第一次启动时就不用去解压程序压缩包的程序,少了一个解压的过程,加快启动速度。为什么说是第一次呢?因为 dex 版本只有第一次会解压执行程序到 data/dalvik-cache(针对 PathClassLoader)或者 optimizedDirectory(针对 DexClassLoader)目录,之后也是直接读取目录下的 dex 文件,所以第二次启动就和正常的差不多了。当然这只是简单的理解,实际的 odex 还有一定的优化作用。ClassLoader 只能加载内部存储路径中的 dex 文件,所以这个路径必须为内部路径。

libraryPath:指目标类中所使用的 c/c++ 库存放的地址。

parcent:指该加载器的父加载器,一般为当前执行类的加载器,例如在 Android 中以 context.getClassLoader() 作为父加载器。

2.4 PathClassLoader

public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
    super(dexPath, null, libraryPath, parcent);
}

可以看出 PathClassLoader 将 optimizedDirectory 置为 null。这时候 optimizedDirectory 会使用 dexPath 的值。

据测试,PathClassLoader 在 dalvik 上只能加载已经安装的 apk,在 art 上则没有这个限制。

在 App 的 Activity 页面中调用 getClassLoader,返回的就是 PathClassLoader 对象,它的父加载器是 BootClassLoader。

2.5 DexClassLoader

DexClassLoader 支持加载包含 classes.dex 的 jar/apk 文件,可以是 sd 卡上的路径。

上面说 dalvik 不能直接识别 jar,DexClassLoader 却可以加载 jar 文件,这难道不矛盾吗?其实在 BaseDexClassLoader 里对“.jar”、“.zip”、“.apk”、“.dex”后缀的文件最后都会生成一个对应的 dex 文件,所以最终处理的还是 dex 文件,而 URLClassLoader 并没有做类似的处理,一般我们都是使用 DexClassLoader 作为动态加载的加载器。

注意:
optimizedDirectory 在 8.1 及以上已经被废弃了,DexClassLoader 调用父类构造方法时也是固定传递 null,所以在 8.1 及以上 DexClassLoader 和 PathClassLoader 没有区别。

2.6 InMemoryDexClassLoader

public InMemoryDexClassLoader(ByteBuffer[] dexBuffers, ClassLoader parcent) {
    super(dexBuffers, parcent);
}

InMemoryDexClassLoader 是在 API26 时新增的类加载器,继承自 BaseDexClassLoader。

InMemoryDexClassLoader 的构造函数调用其父类 BaseDexClassLoader 的构造函数,ByteBuffer 数组构造了一个 DexPathList,可以用于内存中的 dex 文件。

2.7 DelegateLastClassLoader

DelegateLastClassLoader 是在 API27 时新增的类加载器,继承自 PathClassLoader。
DelegateLastClassLoader 实行最后查找策略。使用 DelegateLastClassLoader 来加载每个类或资源,使用以下查找顺序:

首先,判断是否已经加载过该类。
然后,搜索此类的类加载器是否加载过该类。
最后,搜索与该类加载器的 dexPath 关联的 dex 文件列表,委托给指定的父对象加载。

三、ClassLoader 的双亲委托模型

双亲委托模型是指先委托父加载器寻找目标类,只有在找不到的情况下才从自己的类路径下查找并加载目标类。

Java Launcher 类中,初始化时会生成一个 ExtClassLoader 和 一个 AppClassLoader,BootstrapClassLoader 作为 ExtClassLoader 的双亲,ExtClassLoader作为 AppClassLoader 的双亲。

加载类时使用 AppClassLoader,AppClassLoader 去询问 ExtClassLoader,ExtClassLoader 去询问 BootstrapClassLoader,如果它们都不处理,那么 AppClassLoader 自己去处理。

由此实现一种优先级,BootstrapClassLoader > ExtClassLoader > AppClassLoader。

更多可见一句话讲清楚双亲委托模型

3.1 实现

实现就在 ClassLoader 的 loadClass() 方法。

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) {
            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.
                c = findClass(name);
            }
        }
        return c;
}
  1. 判断目标类是否被加载过,加载过了就直接返回。
  2. 委托父加载器去加载,加载成功就直接返回。
  3. 自己去加载。

3.2 好处

  1. 通过第一步,可避免重复加载
  2. 通过二、三步,使 Java 的基础环境(JDK 类)的加载都由模型最顶端的启动类加载器完成,保证系统行为统一,即所有使用到 JDK 类的代码中,引用到的都是同一个类。

测试:

  1. 自己编写一个 java.lang.String 类,实现一个 toString() 方法,再打印一个 String。能编译通过,但自己的 String 类的 toString() 方法没有被调用。
public class String {
    @Override
    public String toString() {
        return "bbb";
    }
}

public class Main {
    public static void main(String[] args) {
        String s = "aaa";
        System.out.println(s);
    }
}

// 输出
aaa
  1. 自己编写一个 java.lang.String 类,实现一个 print() 方法,再调用它。能编译通过,但执行时会抛找不到方法的异常。
public class String {
    public void print() {
        System.out.println("ccc");
    }
}

public class Main {
    public static void main(String[] args) {
        String s = "aaa";
        s.print();
    }
}

// 输出:
Exception in thread "main" java.lang.NoSuchMethodError: 
	java.lang.String.print()V
		at com.gdeer.classloadertest.Main.main(Main.java:6)
  1. 自己编写一个空的 java.lang.String 类。调用系统 String 的方法,如 String.length(),编译不通过。
public class String {
}

public class Main {
    public static void main(String[] args) {
        String s = "aaa";
        System.out.println(s.length());
    }
}

// 编译错误
错误: 找不到符号

综以上三条测试所述,java.lang.String 类的加载总会使用系统类加载器,系统类加载器会从系统路径加载 java.lang.String

拓展开来,系统类的加载总会使用系统类加载器,系统类加载器会从系统路径加载系统的类。

  1. 自己编写一个 ClassLoader,强行用 defineClass() 方法加载系统的 java.lang.String。会收到一个java.lang.SecurityException: Prohibited package name: java.lang 异常。

3.3 打破

双亲委托模型只是 JDK 提供的 ClassLoader 类的实现方式,用于 JDK 的系统类加载。在实际开发过程中,我们可以通过自定义 ClassLoader 并重写父类的 loadClass 方法来打破这一机制。

3.4 双亲委托模型在热修复领域的应用

一个 ClassLoader 可以有多个 dex 文件,每个 dex 文件是一个 element,多个 dex 文件排成一个有序数组 dexElements,当找类的时候,会按照顺序遍历 dex 文件,然后从当前遍历的 dex 文件中寻找类,由于双亲委托模型,只要找到就会停止查找并返回,如果找不到就从下一个 dex 文件继续找。只要我们先加载修复好的 dex 文件,那么就不会加载有 bug 的 dex 文件了。

另外,假设 app 中有个类叫做 A,在其内部引用了 B,发布过程中发现 B 中有编写错误,那么想要发布一个新的 B 类,就要阻止 A 类打上 CLASS_ISPREVERIFIED 的标志。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值