Android——类加载机制

Java中的类加载器

Java类加载器是 Java 运行时环境(Java Runtime Environment)的一部分,它负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。

Java中的类加载器是java.lang.ClassLoader,它是一个抽象类。给定一个类名,ClassLoader就负责把这个类从特定的文件系统中加载到虚拟机中。

Class类有一个方法getClassLoader(),每一个类的 Class 对象都可以调用这个方法来获取把这个类加载到虚拟机中的 ClassLoader。

对于数组来说,它们不是由 ClassLoader 来创建,而是由Java运行时创建。数组的 ClassLoader 就是加载该数组元素类的 ClassLoader。如果元素类型是基本类型,那么数组就没有 ClassLoader。

ClassLoader 采用的是代理模式来加载类,每一个 ClassLoader 实例都有一个父ClassLoader(并不是继承关系),当一个类加载器需要加载一个类的时候,它会首先传递这个类的信息到 parent 类加载器,请求 parent 来加载,然后依次传递,直到该类被成功加载或者失败。如果失败了,那么就由最开始的那个类加载器来进行加载。在Java虚拟机中有一个内置的类加载器是bootstrap classloader,它是没有 parent 的,但是可以作为所有 ClassLoader 实例的 parent。这种加载方式也叫作双亲委派机制或者父委托机制

通常来讲,类加载器都是加载本地的 Class 文件,但是它也可以加载其它来源的文件,比如从网络下载下来的。

可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求而不需要完全了解Java虚拟机的类加载的细节。

ClassLoader 的一个方法defineClass()可以把一个字节数组转为 Class 实例。然后可以根据Class.newInstance()方法来创建一个对象。

被ClassLoader创建的类的方法或者构造方法可能还会引用其它的类,为了确定引用的类,虚拟机会调用最开始加载引用类的 ClassLoader 的loadClass()方法。

protected final Class defineClass(String name, byte[] b, int off, int len)

例如,想要自定义一个 NetworkClassLoader,来加载从网络传来的Class类:

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

NetworkClassLoader 必须重写findClass()方法,,然后定义一个方法来返回Class类的字节数组。当下载完毕,需要调用defineClass方法,示例如下:

class NetworkClassLoader extends ClassLoader {
    String host;
    int port;

    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) {
        // load the class data from the connection
          . . .
    }
}

JVM中的ClassLoader

JVM中有3个默认的类加载器:

  • 引导(Bootstrap)类加载器
    用C/C++写的,在Java代码中无法获取到。主要是加载存储在/jre/lib目录下的核心Java库,对应的加载路径是sun.boot.class.path。
  • 扩展(Extensions)类加载器.
    用来加载/jre/lib/ext目录下或者对应的加载路径java.ext.dirs中指明的Java扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。该类由sun.misc.Launcher$ExtClassLoader实现。
  • Apps类加载器(也称系统类加载器)
    根据 Java应用程序的类路径(java.class.path或CLASSPATH环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。该类由sun.misc.Launcher$AppClassLoader实现,它的 parent 类加载器是ExtClassLoader

ClassLoader源码分析

类的加载使用的是双亲委派机制。那我们启动一个Java应用程序,它的类加载顺序是从AppClassLoader委托ExtClassLoader,如果ExtClassLoader也找不到就会去委托Bootstrap类加载器加载。如果父加载器没有找到的话,再从子加载器中加载,加载到的类会被缓存起来,如果最终都没有找到这个类,就会报一个异常ClassNotFoundException

ClassLoader的构造方法,它有3个构造方法,但是其中有一个私有的:

//最终调用的还是这个私有的方法
private ClassLoader(Void unused, ClassLoader parent) {}

//有参构造,传递parent类加载器
protected ClassLoader(ClassLoader parent) {
   this(checkCreateClassLoader(), parent);
}

// 无参构造,默认采用getSystemClassLoader()方法获取的ClassLoader作为parent类加载器
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

public static ClassLoader getSystemClassLoader() {
    // 初始化系统类加载器
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    // 做一些安全方面的校验
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}

ClassLoader 加载一个类,调用的方法是loadClass()方法

需要指出:类加载过程是同步的

简单总结一下类加载器的工作过程:

  1. 如果当前加载的类已经加载过,直接从缓存获取。
  2. 之前没有加载过,如果该 ClassLoader 对象的 parent 不为 null 就委托父加载器加载,父加载器会重新开始走第1步。如果 parent 为 null,那么就采用根加载器 bootstrap class loader 进行加载。
  3. 如果之前还是没有成功加载类,那么就会调用当前 ClassLoader 的findClass()方法去加载。

类加载器采用双亲委派机制的好处:

  1. 加载的类会被缓存起来,下次加载就快了。
  2. 安全,比如我们自定义一个与系统String包名类型一致的类,然后想要把这个String类加载进来干点坏事的话实际上是做不到的。由于父委托机制,真正的String类会被bootstrap class loader 加载(String类是存放在bootstrap class loader 负责加载的区域),就不会再调用我们这个假的String类。
    实际上,如果你自定义了一个类加载器并且重写了 loadClass 的逻辑,最终还是不能加载假的 String 类,因为 ClassLoader 有一个preDefineClass()方法,该方法会检测类的包名,如果是'java'开头就会抛出一个SecurityException异常。

那么 AppClassLoader 和 ExtClassLoader 是什么时候初始化的呢?下面我们再去看一下Launcher的部分源码:

自定义类加载器

我们完全可以通过自定义类加载器来加载我们想要加载的类,这个类可能来源于网络,也可能来源于文件系统。

从前面的分析我们知道,加载一个类的过程调用的是 ClassLoader 的 loadClass() 方法。自定义类加载器通常不要重写 loadClass() 方法的逻辑。在这个方法内部,如果所有的父加载器都没有成功加载,就会调用 ClassLoader 对象自身的 findClass() 方法,自定义类加载器实现这个 findClass() 方法即可。

还有一个关键的方法就是调用 ClassLoader 对象的 defineClass() 方法,这样就可以创建一个Class对象了。

扩展知识点

在Java中,类的加载时按需加载,也就是需要的时候才会把class文件加载到内存中。可以分为隐式加载和显示加载。

  • 隐式加载:由当 new 一个 Java 对象,或者调用类的静态方法或者使用静态成员变量的时候,会加载当前的 Class。
  • 显式加载:显示的调用 Class.forName() 方法,或者调用 ClassLoader 的 loadClass() 方法。

Android中的ClassLoader

Java 中的 ClassLoader 是加载 class 文件,而 Android 中的虚拟机无论是 dvm 还是 ART 都只能识别 dex 文件。因此 Java 中的 ClassLoader 在 Android 中不适用。

Android 中的java.lang.ClassLoader这个类也不同于 Java 中的 java.lang.ClassLoader。

Android 中的 ClassLoader 类型也可分为 系统ClassLoader自定义ClassLoader。其中 系统ClassLoader 包括3种,分别是:

  1. BootClassLoade
    Android 系统启动时会使用 BootClassLoader 来预加载常用类,与 Java 中的 Bootstrap ClassLoader 不同的是,它并不是由C/C++代码实现,而是由 Java 实现的。BootClassLoader 是 ClassLoader 的一个内部类。
  2. PathClassLoader,全名是dalvik/system.PathClassLoader
    可以加载已经安装的Apk,也就是 /data/app/package 下的 apk 文件,也可以加载 /vendor/lib, /system/lib 下的 nativeLibrary。
  3. DexClassLoader,全名是dalvik/system.DexClassLoader
    可以加载一个未安装的apk文件。

PathClassLoader 和 DexClasLoader 都是继承自
dalviksystem.BaseDexClassLoader,它们的类加载逻辑全部写在 BaseDexClassLoader 中。

ClassLoader源码分析

在 Android 中主要关心的是 PathClassLoader 和 DexClassLoader。

PathClassLoader 用来操作本地文件系统中的文件和目录的集合。并不会加载来源于网络中的类。Android 采用这个类加载器一般是用于加载系统类和它自己的应用类。这个应用类放置在data/data/包名下。

看一下PathClassLoader的源码,只有2个构造方法:

package dalvik.system;

public class PathClassLoader extends BaseDexClassLoader {

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

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

DexClassLoader可以加载一个未安装的APK,也可以加载其它包含dex文件的JAR/ZIP类型的文件。
DexClassLoader 需要一个对应用私有且可读写的文件夹来缓存优化后的class文件。而且一定要注意不要把优化后的文件存放到外部存储上,避免使自己的应用遭受代码注入攻击。看一下它的源码,只有1个构造方法:

package dalvik.system;
import java.io.File;
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以看到,PathClassLoader 和 DexClassLoader 除了构造方法传参不同,其它的逻辑都是一样的。要注意的是 DexClassLoader 构造方法第2个参数指的是dex优化缓存路径,这个值是不能为空的。而 PathClassLoader 对应的dex优化缓存路径为null是因为Android系统自己决定了缓存路径。

Android 中具体负责类加载的并不是哪个 ClassLoader,而是通过 DexFiledefineClassNative() 方法来加载的。

BaseDexClassLoader的构造方法有四个参数:

  1. dexPath,指的是在 Android 包含类和资源的jar/apk类型的文件集合,指的是包含dex文件。多个文件用“:”分隔开,用代码就是 File.pathSeparator 。
  2. optimizedDirectory,指的是odex优化文件存放的路径,可以为null,那么就采用默认的系统路径。
  3. libraryPath,指的是native库文件存放目录,也是以“:”分隔。
  4. parent,parent类加载器

可以看到,在 BaseDexClassLoader 类中初始化了 DexPathList 这个类的对象。这个类的作用是存放指明包含dex文件、native库和优化目录。

# dalvik.system.BaseDexClassLoader
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

dalvik.system.DexPathList封装了dex路径,是一个final类,而且访问权限是包权限,也就是说外界不可继承,也不可访问这个类。

BaseDexClassLoader 在其构造方法中初始化了 DexPathList 对象,我们来看一下 DexPathList 的源码,我们需要重点关注一下它的成员变量 dexElements ,它是一个 Element[] 数组,是包含dex的文件集合。Element 是 DexPathList 的一个静态内部类。DexPathList 的构造方法有4个参数。从其构造方法中也可以看到传递过来的 classLoade 对象和 dexPath 不能为null,否则就抛出空指针异常。

小结一下:在 BaseDexClassLoader 对象构造方法内,创建了 PathDexList 对象。而在 PathDexList 构造方法内部,通过调用一系列方法,把直接包含或者间接包含dex的文件解压缩并缓存优化后的dex文件,通过 PathDexList 的成员变量 Element[] dexElements 来指向这个文件。

我们来看一下在 Android 中 ClassLoader 的 loadeClass() 方法。
与在 Java 中的 loadClass() 方法主要流程是类似的,不过因为 Android 中 BootClassLoader 是用Java代码写的,所以可以直接当作系统类加载器的 parent 类加载器。在 Android 中如果 parent 类加载器找不到类,最终还是会调用 ClassLoader 对象自己的 findClass() 方法。这个与在Java中逻辑是一样的。

BaseDexClassLoader 类的 findClass() 方法,实际上 BaseDexClassLoader 调用的是其成员变量 DexPathList pathList 的 findClass() 方法。

实际上 DexPathList 最终还是遍历其自身的 Element[] 数组,获取 DexFile 对象来加载 Class 文件。我们之前讲 DexPathList 构造方法内是调用其 makeDexElements() 方法来创建 Element[] 数组的,而且也提到了如果zip文件或者dex文件二者之一不为null,就把元素添加进来,而添加进来的zip存在不为null也不包含dex文件的可能。从上面的代码中也可以看到,获取 Class 的时候跟这个zip文件没什么关系,调用的是dex文件对应的 DexFile 的方法来获取 Class。

数组的遍历是有序的,假设有两个dex文件存放了二进制名称相同的Class,类加载器肯定就会加载在放在数组前面的dex文件中的Class。现在很多热修复技术就是把修复的dex文件放在DexPathList中Element[]数组的前面,这样就实现了修复后的Class抢先加载了,达到了修改bug的目的。

Android 加载一个 Class 是调用 DexFile 的 defineClass() 方法。而不是调用 ClassLoader 的 defineClass() 方法。这一点与Java不同,毕竟Android虚拟机加载的dex文件,而不是class文件。

小结一下:
Android 中加载一个类是遍历 PathDexList 的 Element[] 数组,这个 Element 包含了 DexFile ,调用 DexFile 的方法来获取 Class 文件,如果获取到了 Class,就跳出循环。否则就在下一个 Element 中寻找 Class。

Android 中的类加载器是 BootClassLoader、PathClassLoader、DexClassLoader,其中 BootClassLoader 是虚拟机加载系统类需要用到的,PathClassLoader 是App加载自身dex文件中的类用到的,DexClassLoader 可以加载直接或间接包含dex文件的文件,如APK等。

PathClassLoader 和 DexClassLoader 都继承自 BaseDexClassLoader,它的一个 DexPathList 类型的成员变量 pathList 很重要。DexPathList 中有一个 Element 类型的数组 dexElements,这个数组中存放了包含dex文件(对应的是DexFile)的元素。 BaseDexClassLoader 加载一个类,最后调用的是 DexFile 的方法进行加载的。

无论是热修复还是插件化技术中都利用了类加载机制,所以深入理解Android中的类加载机制对于理解这些技术的原理很有帮助。

MultiDex原理解析

MultiDex的由来

Android 中由于一个dex文件最多存储65536个方法,也就是一个short类型的范围,所以随着应用的类不断增加,当一个dex文件突破这个方法数的时候就会报出异常。虽然可以通过混淆等方式来减少无用的方法,但是随着APP功能的增多,突破方法数限制还是不可避免的。

因此在 Android 5.0 时,Android 推出了官方的解决方案:MultiDex。打包的时候,把一个应用分成多个 dex,例如:classes.dex、classes2.dex、classes3.dex...,加载的时候把这些dex都追加到 DexPathList 对应的数组中,这样就解决了方法数的限制。

5.0后的系统都内置了加载多个dex文件的功能,而在5.0之前,系统只可以加载一个主dex,其它的dex就需要采用一定的手段来加载。这也就是我们今天要讲的MultiDex。

MultiDex 存放在 android.support.multidex 包下。

MultiDex的使用
  1. 在主应用的 build.gradle 文件夹添加依赖
  2. 在 AndroidManifest.xml 中的 app 节点下,使用 MultiDexApplication 作为应用入口。
public class MultiDexApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }
}

当然了,大部分情况下,我们都会自定义一个自己的Application对应用做一些初始化。这种情况下,可以在我们自定义的Application中的 attachBaseContext() 方法中调用 MultiDex.install() 方法。

# 自定义的Applicaiton中
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }

需要注意的是:MultiDex.install() 方法的调用时机要尽可能的早,防止加载后面的dex文件中的类时报 ClassNotFoundException。

MultiDex源码分析
分析MultiDex的的入口就是它的静态方法install()。
这个方法的作用就是把从应用的APK文件中的dex添加到应用的类加载器PathClassLoader中的DexPathList的Emlement数组中。

你可能感兴趣的:(Android——类加载机制)