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

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,这个参数的意思是生成的 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 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中的类大家是不是就有思路了?

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

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

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

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

实现方法

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

fun load(context: Context) {

// 获取 pathList

val systemClassLoader = Class.forName(“dalvik.system.BaseDexClassLoader”)

val pathListField = systemClassLoader.getDeclaredField(“pathList”)

pathListField.isAccessible = true

// 获取 dexElements

val dexPathListClass = Class.forName(“dalvik.system.DexPathList”)

val dexElementsField = dexPathListClass.getDeclaredField(“dexElements”)

dexElementsField.isAccessible = true

// 获取宿主的Elements

val hostClassLoader = context.classLoader

val hostPathList = pathListField.get(hostClassLoader)

val hostElements = dexElementsField.get(hostPathList) as kotlin.Array

// 获取插件的Elements

val pluginClassLoader = PathClassLoader(“sdcard/plugin-debug.apk”, context.classLoader)

val pluginPathList = pathListField.get(pluginClassLoader)

val pluginElements = dexElementsField.get(pluginPathList) as kotlin.Array

// 创建数组

val newElements =

Array.newInstance(

pluginElements.javaClass.componentType!!,

hostElements.size + pluginElements.size

) as kotlin.Array

// 给新数组赋值

// 先用宿主的,再用插件的

System.arraycopy(hostElements, 0, newElements, 0, hostElements.size)

System.arraycopy(pluginElements, 0, newElements, hostElements.size, pluginElements.size)

// 将生成的新值赋给 “dexElements” 属性

dexElementsField.set(hostPathList, newElements)

}

这样就合并了两个dex文件的类,宿主中就可以直接加载插件中的类了

private fun loadApk() {

try {

val clazz = Class.forName(“com.kangf.plugin.Test”)

val method = clazz.getMethod(“test”, Context::class.java)

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

} catch (e: Exception) {

e.printStackTrace()

// 调用上面的load方法

Toast.makeText(this, “请先点击加载apk”, Toast.LENGTH_LONG).show()

}

}

好了,下面来看一下运行效果吧!

篇幅限制,今天就讲到这里,还有两个问题(加载资源图片和四大组件)看以下两篇文章。

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

https://blog.csdn.net/qq_22090073/article/details/104053249

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

https://blog.csdn.net/qq_22090073/article/details/104063781

本文源码地址:

https://github.com/plumcookingwine/DynamicTest

最后

==============================================================

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
成体系的自学效果低效漫长且无助**。

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-OTsahjsc-1715882709245)]

[外链图片转存中…(img-blbc94oS-1715882709246)]

[外链图片转存中…(img-aLUIXfDn-1715882709247)]

[外链图片转存中…(img-p5fT1mRz-1715882709248)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值