写点什么

从零开始实现一个插件化框架(一)

发布于: 2021 年 11 月 07 日

ClassLoader 类加载器


以前在讲热修复的时候,我简单地介绍了一下 ClassLoader 的加载机制。java 源码文件在编译后会生成一个 class 文件,而在 Android 中,将代码编译后会生成一个 apk 文件,将 apk 文件解压后就可以看到其中有一个或多个 classes.dex 文件,它就是安卓把所有 class 文件进行合并,优化后生成的。


java 中 JVM 加载的是 class 文件,而安卓中 DVM 和 ART 加载的是 dex 文件,虽然二者都是用的 ClassLoader 加


载的,但因为加载的文件类型不同,还是有些区别的,所以接下来我们主要介绍安卓的 ClassLoader 是如何加载


dex 文件的。


ClassLoader 实现类




在 Android 中,ClassLoader 是一个抽象类,它的实现类主要分为两种类型:系统类加载器(BootClassLoader),和自定义类加载器(PathClassLoader | DexClassLoader)


先看一下 ClassLoader 加载流程图:



BootClassLoader


用于加载 Android Framework 层的 class 文件,比如 Activity、Fragment,不过需要注意的是 AppCompatActivity 虽然也是 google 工程师提供的类,但是一个第三方包中的类,并不输入 Framwork 层,所以 AppCompatActivity 并不是使用 BootClassLoader 加载的


PathClassLoader


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


DexClassLoader


在 Android8.0 以后的 API 中,和 PathClassLoader 是没有任何区别的,而在以前的 API 中,两者只有一个设置加载路径的区别(有的文章说,PathClassLoader 只支持直接操作 dex 格式文件,而 DexClassLoader 可以支持.apk、.jar 和.dex 文件,并且会在指定的 outpath 路径释放出 dex 文件。其实不然,甚至可以说两者没有任何区别)



先放一张 ClassLoader 类继承关系图,相信都能看懂,就不多讲了,下面来看一下 PathClassLoader 和 DexClassLoader 的源码:


// /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java


public class PathClassLoader extends BaseDexClassLoader {


// optimizedDirectory 直接为 null


public PathClassLoader(String dexPath, ClassLoader parent) {


super(dexPath, null, null, parent);


}


// optimizedDirectory 直接为 null


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


super(dexPath, null, librarySearchPath, parent);


}


}


// API 小于等于 26/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java


public class DexClassLoader extends BaseDexClassLoader {


public DexClassLoader(String dexPath, String optimizedDirectory,


String librarySearchPath, ClassLoader parent) {


// 26 开始,super 里面改变了,看下面两个构造方法


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


}


}


// API 26/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java


public BaseDexClassLoader(String dexPath, File optimizedDirectory,


String librarySearchPath, ClassLoader parent) {


super(parent);


// DexPathList 的第四个参数是 optimizedDirectory,可以看到这儿为 null


this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);


}


// API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java


public BaseDexClassLoader(String dexPath, File optimizedDirectory,


String librarySearchPath, ClassLoader parent) {


super(parent);


this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);


}


根据源码就可以了解到,PathClassLoader 和 DexClassLoader 都是继承自 BaseDexClassLoader,且类中只有构造方法,它们的类加载逻辑完全写在 BaseDexClassLoader 中。


其中我们值的注意的是,在 8.0 之前,它们二者的唯一区别是第二个参数 optimizedDirectory,这个参


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


数的意思是


生成的 odex(优化的 dex)存放的路径,PathClassLoader 直接为 null,而 DexClassLoader 是使用用户传进来的


路径,而在 8.0 之后,二者就完全一样了。


下面我们再来了解下 BootClassLoader 和 PathClassLoader 之间的关系:// 在 onCreate 中执行下面代码


ClassLoader classLoader = getClassLoader();


while (classLoader != null) {


Log.e("leo", "classLoader:" + classLoader);


classLoader = classLoader.getParent();


}


Log.e("leo", "classLoader:" + Activity.class.getClassLoader());


打印结果:


classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file


"/data/user/0/com.enjoy.pluginactivity/cache/plugin-debug.apk", zip file


"/data/app/com.enjoy.pluginactivity-T4YwTh-


8gHWWDDS19IkHRg==/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.pluginactivity-


T4YwTh-8gHWWDDS19IkHRg==/lib/x86_64, /system/lib64, /vendor/lib64]]]


classLoader:java.lang.BootClassLoader@a26e88d


classLoader:java.lang.BootClassLoader@a26e88d


通过打印结果可知,应用程序类是由 PathClassLoader 加载的,Activity 类是 BootClassLoader 加载的,并且


BootClassLoader 是 PathClassLoader 的 parent,这里要注意 parent 与父类的区别。这个打印结果我们下面还


会提到。


加载原理


那么如何使用类加载器去从 dex 中加载一个插件类呢?很简单


比如,有一个 apk 文件,路径是 apkPath,里面有个类 com.plugin.Test,就可以通过反射加载一个类:


// 初始化一个类加载器


DexClassLoader classLoader = new DexClassLoader(dexPath, context.getCacheDir().getAbsolutePath, null, context.getClassLoader);


// 获取插件中的类


Class<?> clazz = classLoader.loadClass("com.plugin.Test");


// 调用类中的方法


Method method = clazz.getMethod("test", Context.class)


method.invoke(clazz.newInstance(), this)


dex 中加载类很简单,但是我们需要的是将插件中的 dex 加载到宿主里面,又该怎么做呢?其实原理还是跟热修复一样,下面就以 API 26 Android 8.0 举例,通过源码,看一下 DexClassLoader 类加载器是怎么加载一个 apk 中的 dex 文件的。


通过查找发现,DexClassLoader 并没有加载类的方法,继续看它的父类,最后在 ClassLoader 类中找到了一个 loadClass 方法,看来就是通过这个方法来加载类了:


protected Class<?> loadClass(String name, boolean resolve)


throws ClassNotFoundException


{


// 1. 检测这个类是否已经被加载,如果已经被加载了就可以直接返回了


Class<?> c = findLoadedClass(name);


// 如果类未被加载


if (c == null) {


try {


// 2. 判断是否有上级加载器,使用上级加载器的 loadClass 方法去加载


if (parent != null) {


c = parent.loadClass(name, false);


} else {


// 正常情况下是不会走到这里的,因为最终 ClassLoader 都会走到 BootClassLoader,重写了 loadClass 方法结束掉了递归


c = findBootstrapClassOrNull(name);


}


} catch (ClassNotFoundException e) {


}


// 3. 如果所有的上级都没找到,就调用 findClass 方法去查找


if (c == null) {


c = findClass(name);


}


}


return c;


}


上面类加载分为了 3 个步骤


1、 检测这个类是否已经被加载,最终会调用到 native 方法实现查找,这里就不深入了:


protected final Class<?> findLoadedClass(String name) {


ClassLoader loader;


if (this == BootClassLoader.getInstance())


loader = null;


else


loader = this;


//native 方法


return VMClassLoader.findLoadedClass(loader, name);


}


2、如果没被找到,就会从 parent 中调用 loadClass 方法去查找,依次递归,如果找到了就返回,如果所有的上级都没有找到,又会调用到 findClass 一级一级的去查找。这个过程就是双亲委托机制


3、 findClass


// -->2 加载器一般都会重写这个方法,定义自己的加载规则


protected Class<?> findClass(String name) throws ClassNotFoundException {


throw new ClassNotFoundException(name);


}


根据前面的打印结果我们可以看懂,ClassLoader 的最上级是 BootClassLoader,来 看下它是如何重写的 loadClass 方法,结束递归的:


class BootClassLoader extends ClassLoader {


@Override


protected Class<?> findClass(String name) throws ClassNotFoundException {


return Class.classForName(name, false, null);


}


@Override


protected Class<?> loadClass(String className, boolean resolve)


throws ClassNotFoundException {


Class<?> clazz = findLoadedClass(className);


if (clazz == null) {


clazz = findClass(className);


}


return clazz;


}


}


从上面可以看到 BootClassLoader 重写了 findClass 和 loadClass 方法,并且在 loadClass 方法中,不再获取 parent,从而结束了递归。


接着往下走,如果所有的 parent 都没找到,DexClassLoader 是如何加载的,通过查找,其实现方法在它的父类 BaseDexClassLoader 中:


// /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java


@Override


protected Class<?> findClass(String name) throws ClassNotFoundException {


// 在 pathList 中查找指定的 Class


Class c = pathList.findClass(name, suppressedExceptions);


return c;


}


public BaseDexClassLoader(String dexPath, File optimizedDirectory,


String librarySearchPath, ClassLoader parent) {


super(parent);


// 初始化 pathList


this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);


}


findClass 中有调用了 DexPathList 中的 findClass 方法,继续:


private Element[] dexElements;


public Class<?> findClass(String name, List<Throwable> suppressed) {


//通过 Element 获取 Class 对象


for (Element element : dexElements) {


Class<?> clazz = element.findClass(name, definingContext, suppressed);


if (clazz != null) {


return clazz;


}


}


return null;


}


到这里一目了然,class 对象就是从 Element 中获得的,而每一个 Element 就对应了一个 dex 文件,因为一个 apk 中 dex 文件可能有多个,所以就使用了数组来盛放 Element。到这里加载 apk 中的类大家是不是就有思路了?


创建插件的 ClassLoader 加载器(PathClassLoader 或 DexClassLoader),然后通过反射,获取插件的 dexElements 数组的值


获取宿主的 ClassLoader 加载器,通过反射获取宿主的 dexElements 数组的值。


合并宿主和插件的 dexElements 数组,生成一个新的数组


通过反射将新的数组重新赋值给宿主的 dexElements


实现方法


废话不多说,直接上代码:(我这里使用了 kotlin,写起来感觉方便一些)


fun load(context: Context) {


// 获取 pathList


val systemClassLoader = Class.forName("dalvik.system.BaseDexClassLoader")


val pathListField = systemClassLoader.getDeclaredField("pathList")


pathListField.isAccessible = true


// 获取 dexElements

评论

发布
暂无评论
从零开始实现一个插件化框架(一)