Android类加载机制

学习 Android 的类加载机制前,需要具备以下两个知识点

  1. Java 的类加载机制
  2. Java反射

CLassLoader

我们知道,Android 虚拟机是 Dalvik/ART VM。Dalvik/ART,加载类和资源也是要用到ClassLoader,不过 Jvm 通过 ClassLoader 加载的 class 字节码,而 Dalvik/ART VM 通过ClassLoader 加载则是 dex。

Android 的类加载器分为两种

  • PathClassLoader: 用来加载系统类和应用类
  • DexClassLoader:用来加载jar、apk、dex文件.加载jar、apk也是最终抽取里面的Dex文件进行加载

两者都继承自 BaseDexClassLoader,

PathClassLoader 代码位于libcore\dalvik\src\main\Java\dalvik\system\PathClassLoader.java
DexClassLoader 代码位于libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java
BaseDexClassLoader 代码位于libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java

加载流程

在使用标准Java虚拟机时,我们经常自定义继承自ClassLoader的类加载器。然后通过defineClass方法来从一个二进制流中加载Class。然而,这在Dalvik虚拟机上是行不通的。

Dalvik虚拟机类加载流程如下图所示:

dd

源码分析

PathClassLoader是默认的类加载器,下面我们就来说说两者的区别与联系。

DexClassLoader 和 PathClassLoader 都属于符合双亲委派模型的类加载器(因为它们没有重载loadClass方法)。也就是说,它们在加载一个类之前,会去检查自己以及自己以上的类加载器是否已经加载了这个类。如果已经加载过了,就会直接将之返回,而不会重复加载。

初始化

要加载一个 class,必须先有一个 classLoader,以 DexClassLoader 为例,

/**
*
* @param dexPath 加载APK、DEX和JAR的路径。这个类可以用于Android动态加载DEX/JAR。
* @param optimizedDirectory 是DEX的输出路径。
* @param libraryPath 加载DEX的时候需要用到的lib库,libraryPath一般包括/vendor/lib和/system/lib。
* @param parent DEXClassLoader指定的父类加载器
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

关于DexClassLoader,除了它的构造函数以外,它的源码注释里还提到以下三点:

  1. DexClassLoader 加载的文件是.jar或者.apk文件,并且这个.jar或.apk中是包含 classes.dex 这个入口文件的,主要是用来执行那些没有被安装的一些可执行文件。

  2. DexClassLoader 需要一个属于应用的私有的,可以作为它自己的缓存优化目录,其实这个目录也就作为下面,这个构造函数的第二个参数

  3. 不要把上面第二点中提到的这个缓存目录设为外部存储,因为外部存储容易收到代码注入的攻击。

通过 DexClassLoader 的构造,可以发现会调用父类的构造函数进行初始化,即 BaseDexClassLoader 的构造函数:

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

可以看到,BaseDexClassLoader 也会调用父类的构造,即 ClassLoader 的构造,

protected ClassLoader() {
    this(getSystemClassLoader(), false);
}

/**
 * Constructs a new instance of this class with the specified class loader
 * as its parent.
 *
 * @param parentLoader
 *            The {@code ClassLoader} to use as the new class loader's
 *            parent.
 */
protected ClassLoader(ClassLoader parentLoader) {
    this(parentLoader, false);
}

/*
 * constructor for the BootClassLoader which needs parent to be null.
 */
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
    if (parentLoader == null && !nullAllowed) {
        throw new NullPointerException("parentLoader == null && !nullAllowed");
    }
    parent = parentLoader;
}    

可以看到最后初始化了 parent 这个 ClassLoader,来自 getSystemClassLoader(),即 PathClassLoader

static private class SystemClassLoader {
    public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}

private static ClassLoader createSystemClassLoader() {
    String classPath = System.getProperty("java.class.path", ".");
    String librarySearchPath = System.getProperty("java.library.path", "");

    return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}

除了初始化ClassLoader外,还初始化了一个 DexPathList 对象,用来描述DEX文相关资源文件的条目列表,

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ...

    this.definingContext = definingContext;
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
    if (suppressedExceptions.size() > 0) {
        this.dexElementsSuppressedExceptions =
            suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
    } else {
        dexElementsSuppressedExceptions = null;
    }
    this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}

可以看到 DexPathList 里比较重要 dexElements 是在这里被初始化,热修复就是通过这个做到修复bug的。
dexElements 就是用来记录 dex/resource 路径的。

加载 class

之后在加载 class 时,会先遍历 dexElements, 根据 name 查找 class

public Class<?> findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }
    ...
    return null;
}

Element 里的 findClass 方法,

public Class<?> findClass(String name, ClassLoader definingContext,
            List<Throwable> suppressed) {
    return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                : null;
}

最终会调用 DexFile 里的 defineClass(), 调用 native 方法 defineClassNative,完成 Class 的加载

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                    DexFile dexFile, List<Throwable> suppressed) {
    Class result = null;
    try {
        result = defineClassNative(name, loader, cookie, dexFile);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}