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。

相关推荐
吃着火锅x唱着歌8 分钟前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
ThisIsClark1 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
_Shirley1 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
测试19983 小时前
外包干了2年,技术退步明显....
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
hedalei3 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng3 小时前
安卓多渠道apk配置不同签名
android
Aphasia3114 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试
枫_feng4 小时前
AOSP开发环境配置
android·安卓
叶羽西4 小时前
Android Studio打开一个外部的Android app程序
android·ide·android studio
GISer_Jing5 小时前
2025年前端面试热门题目——HTML|CSS|Javascript|TS知识
前端·javascript·面试·html