前言
插件化技术最初源于免安装运行 apk 的想法,这个免安装的 apk 就可以理解为插件,而支持插件的 app 我们一般叫宿主。宿主可以在运行时加载和运行插件,这样便可以将 app 中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现 app 功能的动态扩展。
插件化的开源框架
插件化发展到现在,已经出现了非常多的框架,下表列出部分框架:
特性 | DynamicAPK | dynamic- load-apk | Small | DroidPlugin | RePlugin | VirtualAPK |
---|---|---|---|---|---|---|
支持四大组件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 | 全支持 |
组件无需在宿主manifest 中预注册 | × | √ | √ | √ | √ | √ |
插件可以依赖宿主 | √ | √ | √ | × | √ | √ |
支持PendingIntent | × | × | × | √ | √ | √ |
Android特性支持 | 大部分 | 大部分 | 大部分 | 几乎全部 | 几乎全部 | 几乎全部 |
兼容性适配 | 一般 | 一般 | 中等 | 高 | 高 | 高 |
插件构建 | 部署aapt | 无 | Gradle插件 | 无 | Gradle插件 | Gradle插件 |
我们在选择开源框架的时候,需要根据自身的需求来,如果加载的插件不需要和宿主有任何耦合,也无须和宿主进 行通信,比如加载第三方 App,那么推荐使用 RePlugin,其他的情况推荐使用 VirtualApk。
插件化的实现
我们如何去实现一个插件化呢?首先我们要知道,插件apk是没有安装的,那我们怎么加载它呢?不知道。。。
没关系,这儿我们还可以细分下,一个 apk 主要就是由代码和资源组成,所以上面的问题我们可以变为:**如何加载插件的类?如何加载插件的资源?**这样的话是不是就有眉目了。然后我们还需要解决类的调用的问题,这个地方主要是四大组件的调用问题。我们都知道,四大组件是需要注册 的,而插件的四大组件显然没有注册,那我们怎么去调用呢?
所以我们接下来就是解决这三个问题,从而实现插件化
- 如何加载插件的类?
- 如何加载插件的资源?
- 如何调用插件类?
类加载(ClassLoader)
我们在学 java 的时候知道,java 源码文件编译后会生成一个 class 文件,而在 Android 中,将代码编译后会生成一个 apk 文件,将 apk 文件解压后就可以看到其中有一个或多个 classes.dex 文件,它就是安卓把所有 class 文件进行合并,优化后生成的。
java 中 JVM 加载的是 class 文件,而安卓中 DVM 和 ART 加载的是 dex 文件,虽然二者都是用的 ClassLoader 加载的,但因为加载的文件类型不同,还是有些区别的,所以接下来我们主要介绍安卓的 ClassLoader 是如何加载dex 文件的。
ClassLoader的实现类
ClassLoader是一个抽象类,实现类主要分为两种类型:系统类加载器和自定义加载器。其中系统类加载器主要包括三种:
- BootClassLoader:用于加载Android Framework层class文件。
- PathClassLoader:用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
- DexClassLoader:用于加载指定的dex,以及jar、zip、apk中的classes.dex
类继承关系如下图:
我们先来看下 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,这个参数的意思是生成的 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 与父类的区别。这个打印结果我们下面还会提到。
加载原理
那我们如何使用类加载器去加载一个类呢? 非常的简单,例如:我们有一个apk文件,路径是 apkPath,然后里面有个类 com.enjoy.plugin.Test,那么我们可以通过如下方式去加载 Test 类:
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,context.getCacheDir().getAbsolutePath(), null, context.getClassLoader());
Class<?> clazz = dexClassLoader.loadClass("com.enjoy.plugin.Test");
因为我们需要将插件的 dex 文件加载到宿主里面,所以我们接下来分析源码,看 DexClassLoader 类加载器到底是怎么加载一个 apk 的 dex 文件的。
通过查找发现,DexClassLoader 类中没有 loadClass 方法,一路向上查找,最后在 ClassLoader 类中找到了改方法,源码如下:(后续源码如无标明,都是 API 26 Android 8.0)
// /libcore/ojluni/src/main/java/java/lang/ClassLoader.java
protected Class<?> loadClass(String name, Boolean resolve)
throws ClassNotFoundException{
// 检测这个类是否已经被加载 --> 1
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClass(name, false);
} else {
// 正常情况下不会走这儿,因为 BootClassLoader 重写了 loadClass 方法,结束了递归
c = findBootstrapClassOrNull(name);
}
}
catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果仍然找不到,就调用 findClass 去查找 --> 2
c = findClass(name);
}
}
return c;
}
// -->1 检测这个类是否已经被加载
protected final Class<?> findLoadedClass(String name) {
ClassLoader loader;
if (this == BootClassLoader.getInstance()) loader = null; else
loader = this;
// 最后通过 native 方法实现查找
return VMClassLoader.findLoadedClass(loader, name);
}