Android 中的 ClassLoader 详解

1. ClassLoader

Android 中的 ClassLoader 加载的是 dex 文件

Android 中的 ClassLoader 分为两种类型,分别是系统类加载器自定义加载器

系统类加载器主要有三种:BootClassLoaderPathClassLoaderDexClassLoader

  1. BootClassLoader

    BootClassLoader 由 Java 实现的,是 ClassLoader 的内部类,用于加载 Android Framework 层的 class文件

    BootClassLoader是一个单例类,并且只有在同一个包中才可以访问,因此我们在应用程序中是无法直接调用的

  2. PathClassLoader

    用于加载 Android 应用程序类加载器。可以加载指定的 dex ,以及jar、zip、apk 中的classes.dex

  3. DexClassLoader

    可以加载指定的 dex ,以及jar、zip、apk 中的classes.dex

以下为 PathClassLoader 和 DexClassLoader 的构造方法;

java 复制代码
public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory,

        String librarySearchPath, ClassLoader parent) {

            super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);

        }

}

public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {

        super(dexPath, null, null, parent);

    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){

        super(dexPath, null, librarySearchPath, parent);

    }

}

PathClassLoader 和 DexClassLoader 都继承自 BaseDexClassLoader。二者唯一区别:创建 DexClassLoader 需要传递一个 optimizedDirectory 参数,并且会将其创建为 File 对象传给 super ,而 PathClassLoader 直接为 null。

optimizedDirectory 参数就是 dexopt 的产出目录。那 PathClassLoader 创建时,这个目录为 null,Android 5.0 以下, optimizedDirectory 为 null 时的默认路径为:/data/dalvik-cache

dexopt :

在Dalvik中虚拟机在加载一个dex文件时,对 dex 文件 进行 验证 和 优化的操作,其对 dex 文件的优化结果变成了 odex(Optimized dex) 文件,这个文件使用了一些优化操作码,和 dex 文件很像。

该目录在安装某个 APK 时,系统会自动在其中存放 odex 文件。而在使用PathClassLoader加载时,对于/data/dalvik-cache这个目录,我们的应用自身并不具备写的权限,因此 PathClassLoader 只能加载已经安装的APK中的dex文件。

而到了ART下,加载方式发生了截然不同的变化,在安装时对 dex 文件执行dex2oat(AOT 提前编译操作),编译为OAT 可执行文件。若在加载时,无法成功加载 oat 文件,则会从原 dex 中加载,因此 ART 下,PathClassLoaderDexClassLoader都能加载任意指定的dex,以及jar、zip、apk中的classes.dex。但是从原dex加载会导致无法dex2oat,加快加载速度,降低运行效率。

到了Android 8.1及以后,此时 DexClassLoader 中 optimizedDirectory 同样固定传递null,两者没有任何区别了。

2. ClassLoader 的继承关系

3. ClassLoader 的加载过程

双亲委托模式:首先判断该Class是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次进行递归,直到委托到最顶层的 ClassLoader, 如果最顶层的 ClassLoader 找到了该Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找。

双亲委托模式优点

  1. 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
  2. 安全性考虑,防止核心API库被随意篡改。

Android 的 ClassLoader 遵循了双亲委托模式 ,ClassLoader 的加载方法为抽象类 ClassLoader 中的 loadClass 方法:

java 复制代码
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        //找缓存,找到return
        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;
}

首先,在第5行处,找缓存中是否存在该类,findLoadedClass(name)方法最终会调用到本地方法native static Class findLoadedClass(ClassLoader cl, String name),若传入的类已经加载,则返回该类。如不存在,则在第8行判断父加载器是否存在,第9行,parent 为 ClassLoader 对象,parent 为当前加载器的父类加载器,存在就调用父类加载器中的parent.loadClass(name, false)方法。父类加载器不存在就调用findBootstrapClassOrNull(name)方法,该方法会直接返回null。如果28行 c == null,则说明向上委托流程没有检查出类已经被加载,接着执行21行 findClass(name)方法自己来进行查找。

现在若要查看自己写的类是否被加载,自己定义的类在APP中,在 dex 文件中,通过 parent.loadClass(name, false) 是寻找不到的 ,最后通过 PathClassLoader 的 findClass(name) 方法寻找。

java 复制代码
public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {

        super(dexPath, null, null, parent);
    }

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

PathClassLoader 在初始化时传入了一个地址 dexPath,dexPath地址对应的就是当前程序对应的APK文件。所以 PathClassLoader 能加载到你自己在程序中定义的类。

查看 ClassLoader 中的 findClass 方法:

java 复制代码
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

该方法需要子类进行实现,PathClassLoader 类中没有重写findClass 方法,在 PathClassLoader 的父类BaseDexClassLoader 中实现了 findClass 方法:

java 复制代码
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {

    super(parent);
    //创建了DexPathList,地址 dexPath 传进DexPathList
    this.pathList = new DexPathList(this, dexPath, librarySearchPath,
    optimizedDirectory);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //查找指定的class
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" +
    name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
        cnfe.addSuppressed(t);
        }

    throw cnfe;

    }

return c;

}

那么在 BaseDexClassLoader 中是如何找到自定义的类?第14行代码,Class c = pathList.findClass(name, suppressedExceptions)去寻找类,pathList又是什么呢?第6行代码,在 BaseDexClassLoader 的构造方法中创建了 pathList,并将 dex 地址 dexPath 传入,其实 pathList 是一个 DexPathList 对象。查看 DexPathList 类,在初始化

java 复制代码
private Element[] dexElements;
//构造方法
public DexPathList(ClassLoader definingContext, String dexPath,
        String librarySearchPath, File optimizedDirectory) {

    //...
    // splitDexPath 实现为返回 List<File>
    // makeDexElements 会去 List<File>.add(dexPath) 中使用DexFile加载dex文件返回 Element数组
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
        suppressedExceptions, definingContext);

    //...

}

在 DexPathList 的构造方法中,第9行,其中 splitDexPath 方法进行了分组操作,传入的 dex 文件地址可能有多个,将 dexPath 中不同的dex文件地进行拆分, 并返回一个 List<File> 集合。makeDexElements 方法返回的是一个 Element[]。一个dex文件就是一个Element对象,多个dex就是一个 Element[] 数组。这时再看 BaseDexClassLoader 中的 findClass 方法的pathList.findClass(),该方法调用的是 DexPathList 中的 findClass 方法:

java 复制代码
public Class findClass(String name, List<Throwable> suppressed) {

    //从element中获得代表Dex的 DexFile
    for (Element element : dexElements) {

        DexFile dex = element.dexFile;

        if (dex != null) {

            //查找class
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);

            if (clazz != null) {

                return clazz;

            }

        }

    }

    if (dexElementsSuppressedExceptions != null) {

        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));

    }

    return null;
}

方法第4行对 dexElements 数组进行循环访问,然后通过第6行DexFile dex = element.dexFile拿到具体的dex文件,然后通过dex 再去寻找具体的类,找到了返回该类,没找到返回null。

相关推荐
Java技术小馆10 分钟前
GitDiagram如何让你的GitHub项目可视化
java·后端·面试
UGOTNOSHOT20 分钟前
7.4项目一问题准备
面试
福柯柯30 分钟前
Android ContentProvider的使用
android·contenprovider
不想迷路的小男孩31 分钟前
Android Studio 中Palette跟Component Tree面板消失怎么恢复正常
android·ide·android studio
餐桌上的王子32 分钟前
Android 构建可管理生命周期的应用(一)
android
菠萝加点糖36 分钟前
Android Camera2 + OpenGL离屏渲染示例
android·opengl·camera
用户2018792831671 小时前
🌟 童话:四大Context徽章诞生记
android
yzpyzp1 小时前
Android studio在点击运行按钮时执行过程中输出的compileDebugKotlin 这个任务是由gradle执行的吗
android·gradle·android studio
aningxiaoxixi1 小时前
安卓之service
android
TeleostNaCl2 小时前
Android 应用开发 | 一种限制拷贝速率解决因 IO 过高导致系统卡顿的方法
android·经验分享