【Android】Android中的ClassLoader

Java中的ClassLoader

任何的Java程序都是由若干的.class文件组成的完整Java程序。程序运行时需要将.class文件加载到JVM中使用。而负责加载.class文件的就是ClassLoader机制。

img

ClassLoader的作用简单来说就是加载.class文件,提供给程序运行时使用。

双亲委托模型(Parent Delegation Model)

我们看到JDK中ClassLoader的构造方法,需要传入一个父类的加载器,并且持有这个引用:

protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}

当类加载器收到加载类或者资源的请求时,通常先委托给父类加载器,当父类加载器找不到指定的类或者资源的时候,才会交由自身执行类架子啊过程。

具体过程如下:

  1. 源ClassLoader先判断这个Class是否已经加载,如果已经加载过,则直接返回Class,没有则委托父类加载器
  2. 以此类推直到始祖加载器(引用类加载器)
  3. 如果始祖加载器发现没有加载过这个Class,则从对应路径下找到对应的class字节码文件载入,如果载入失败,则交给子类加载器
  4. 以此类推直到源ClassLoader
  5. 源ClassLoader如果载入成功,则直接返回Class,如果载入失败,则不会再委托其子类加载器,而是抛出异常

Android中的ClassLoader

Android中的Dalvik/ART虚拟机也是需要加载class文件到内存中使用,但在ClassLoader的加载细节上与传统JVM有所区别。

dex文件

应用打包为apk时,class文件会被打包为一个或多个的dex文件。当我们把apk的后缀名改为zip后(apk本质上就是zip文件),里面会有class文件及dex文件。而MulitiDex解决65535问题就是通过生成多个dex文件。

Android安装应用时会根据不同的CPU平台对Dex做出不同的优化。这个过程由一个名为DexOpt的工具处理。DexOpt是第一次加载Dex时执行的,会生成一个ODEX文件。这种ODEX文件的执行效率是比普通Dex文件要高很多的,通过这样一种方式,就可以加快App的启动和相应速度。

而class文件被打包为dex文件,则是通过SDK中的一个叫dx.jar的工具进行的,我们可以在class文件的根目录下用如下的命令将其打包为dex

dx --dex --output=XXX.dex XXX.class

Android中的Dalvik/ART是无法像普通JVM一样直接加载class文件的,只能通过dex来加载。因此,Android中ClassLoader的工作交给了BaseDexClassLoader来处理。

下面的几篇文章详细讲述了ODEX

BaseDexClassLoader

ClassLoader是一个抽象类,其具体实现有两个,分别是BaseDexClassLoaderSecureClassLoader

SecureClassLoader的子类是URLClassLoader,它只能加载jar文件,在Android中无法使用。

BaseDexClassLoader的子类分别是PathClassLoader和DexClassLoader。

PathClassLoader

PathClassLoader在应用创建的时候,从data/app/…的安装目录下,加载apk文件。它有两个构造函数,并且遵从了双亲委托模型:

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

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

其中有两个参数我们需要解释一下:

  • dexPath:它是一个包含了dex的jar文件或者apk文件的路径集,多个路径用分割符号隔开。分隔符通常为":"。
  • libraryPath:它是一个包含了C/C++库的路径集,多个路径也是一样的处理方法。
  • parent:父类加载器,遵从双亲委托模型

PathClassLoader中只有这两个构造方法,具体的实现都是在BaseDexClassLoader中。

BaseDexClassLoader中,dexPath比较受限制,一般是已经安装的apk路径。

其实,在App安装到手机后,apk中的class.dex中的class都是通过PathClassLoader来加载。

DexClassLoader

DexClassLoader对比PathClassLoader来说,PathClassLoader只能加载已经安装应用的dex或者apk文件,而DexClassLoader没有这个限制,可以从SD卡中加载包含class.dex的jar或apk文件(插件化和热修复都是通过这个原来来实现的)。不需要安装应用,即可完成dex的加载。

DexClassLoader中只有一个构造方法,仍然遵循双清委托模型

public DexClassLoader(String dexPath, String optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

我们需要解释一下参数

  • dexPath:包含了class.dex的apk、jar路径,多个用分隔符隔开。
  • OptimizedDirectory:用于缓存优化的dex文件路径,就是从jar或apk中提取出来的dex文件路径,不可为空,应该是应用私有的,有读写权限的路径(外部存储空间也可以,但有代码注入的风险)。通过下面的方式我们可以创建一个路径(API21以上):
File dexOutputDir = context.getCodeCacheDir();
  • libraryPath:存储C/C++文件的路径集。
  • parent:父类加载器,遵从双亲委托模型。

刚刚我们说到,DexClassLoader和PathClassLoader都是对BaseDexClassLoader的一层简单的封装,真正的实现都在BaseDexClassLoader中。

BaseDexClassLoader源码分析

其结构如下:

img

其中,有一个很重要的字段pathList。它继承了ClassLoader,实现了findClass()findResource()方法。

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        ...
        return c;
    }
    @Override
    protected URL findResource(String name) {
        return pathList.findResource(name);
    }
    @Override
    protected Enumeration<URL> findResources(String name) {
        return pathList.findResources(name);
    }
    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

重要的部分都在DexPathList中了,它的构造方法和之前的类似:

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

它调用了makePathElements()方法生成了Element数组,Element是DexPathList的嵌套类,有下面的字段:

static class Element {
    private final File dir;
    private final boolean isDirectory;
    private final File zip;
    private final DexFile dexFile;
    private ZipFile zipFile;
    private boolean initialized;
}

关于如何生成Element数组,我们可以看到makePathElement的源码:

private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                          List<IOException> suppressedExceptions) {
    List<Element> elements = new ArrayList<>();
    // 遍历所有的包含 dex 的文件
    for (File file : files) {
        File zip = null;
        File dir = new File("");
        DexFile dex = null;
        String path = file.getPath();
        String name = file.getName();
        // 判断是不是 zip 类型
        if (path.contains(zipSeparator)) {
            String split[] = path.split(zipSeparator, 2);
            zip = new File(split[0]);
            dir = new File(split[1]);
        } else if (file.isDirectory()) {
            // 如果是文件夹,则直接添加 Element,这个一般是用来处理 native 库和资源文件
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()) {
            // 直接是 .dex 文件,而不是 zip/jar 文件(apk 归为 zip),则直接加载 dex 文件
            if (name.endsWith(DEX_SUFFIX)) {
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                // 如果是 zip/jar 文件(apk 归为 zip),则将 file 值赋给 zip 字段,再加载 dex 文件
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(dir, false, zip, dex));
        }
    }
    // list 转为数组
    return elements.toArray(new Element[elements.size()]);
}

其中,loadDexFile()方法会调用Native层的方法来读取dex文件,详细的可以看 从源码分析 Android dexClassLoader 加载机制原理这篇文章。

接下来我们看到DexPathList的findClass()方法,它根据传入的完整类名加载对应的class。

public Class findClass(String name, List<Throwable> suppressed) {
    // 遍历 dexElements 数组,依次寻找对应的 class,一旦找到就终止遍历
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    // 抛出异常
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
} 

这里的话有一个热修复的实现原理的点,我们可以将热修复的补丁dex文件放到dexElements数组前面,这样加载class的时候,会优先找到补丁包的dex文件,加载到class后不再寻找,原来的apk中的同名类就不再使用。

这样,BaseDexClassLoader寻找class路径就很清晰了:

  1. 传入一个完整的类名,调用 BaseDexClassLader 的 findClass(String name)方法
  2. BaseDexClassLader 的 findClass 方法会交给 DexPathList 的 findClass(String name, List<Throwable> suppressed方法处理
  3. 在 DexPathList 方法的内部,会遍历 dexFile ,通过 DexFile 的 dex.loadClassBinaryName(name, definingContext, suppressed) 来完成类的加载

注意事项

在项目中使用BaseDexClassLoader或者DexClassLoader加载dex或者class时,无法调用findClass()方法,它有包访问权限,我们需要调用loadClass(String className),它其实是BaseDexClassLoader的父类ClassLoader实现的。

public Class<?> loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, false);
}

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = findLoadedClass(className);
    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }
        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }
    return clazz;
}
点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注

%d 博主赞过: