预备知识
- 了解 android 基本开发
- 了解 android 四大组件基本原理
- 了解 ClassLoader 相关知识
看完本文可以达到什么程度
- 了解插件化常见的实现原理
阅读前准备工作
- clone CommonTec 项目
文章概览
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gB0axkS9-1629895859810)(https://user-gold-cdn.xitu.io/2019/7/8/16bd22de0ea2a81c?imageView2/0/w/1280/h/960/ignore-error/1)]
一、插件化框架历史
整个插件化框架历史部分参考了包建强在 2016GMTC 全球开发大会上的演讲
2012 年 AndroidDynamicLoader 给予 Fragment 实现了插件化框架,可以动态加载插件中的 Fragment 实现页面的切换。
2013 年 23Code 提供了一个壳,可以在壳里动态化下载插件然后运行。
2013 年 阿里技术沙龙上,伯奎做了 Atlas 插件化框架的分享,说明那时候阿里已经在做插件化的运用和开发了。
2014 年 任玉刚开源了 dynamic-load-apk,通过代理分发的方式实现了动态化,如果看过 Android 开发艺术探索这本书,应该会对这个方式有了解。
2015 年 张勇 发布了 DroidPlugin,使用 hook 系统方式实现插件化。
2015 年 携程发布 DynamicApk
2015 - 2016 之间(这块时间不太确定),Lody 发布了 VirtualApp,可以直接运行未安装的 apk,基本上还是使用 hook 系统的方式实现的,不过里面的实现要精致很多,实现了自己的一套 AMS 来管理插件 Activity 等等。
2017 年阿里推出 Atlas
2017 年 360 推出 RePlugin
2017 年滴滴推出 VirtualApk
2019 年腾讯推出了 Shadow,号称是零反射,并且框架自身也可实现动态化,看了代码以后发现,其实本质上还是使用了代理分发生命周期实现四大组件动态化,然后抽象接口来实现框架的动态化。后面有机会可以对其做一下分析。
这基本上就是插件化框架的历史,从 2012 至今,可以说插件化技术基本成型了,主要就是代理和 hook 系统两种方式(这里没有统计热修复的发展,热修复其实和插件化还是有些想通的地方,后面的文章会对热修复进行介绍)。如果看未来的话,斗胆预测,插件化技术的原理,应该不会有太大的变动了。
二、名词解释
在插件化中有一些专有名词,如果是第一次接触可能不太了解,这里解释一下。
宿主
负责加载插件的 apk,一般来说就是已经安装的应用本身。
StubActivity
宿主中的占位 Activity,注册在宿主 Manifest 文件中,负责加载插件 Activity。
PluginActivity
插件 Activity,在插件 apk 中,没有注册在 Manifest 文件中,需要 StubActivity 来加载。
三、使用 gradle 简化插件开发流程
在学习和开发插件化的时候,我们需要动态去加载插件 apk,所以开发过程中一般需要有两个 apk,一个是宿主 apk,一个是插件 apk,对应的就需要有宿主项目和插件项目。
在 CommonTec 这里创建了 app 作为宿主项目,plugin 为插件项目。为了方便,我们直接把生成的插件 apk 放到宿主 apk 中的 assets 中,apk 启动时直接放到内部存储空间中方便加载。
这样的项目结构,我们调试问题时的流程就是下面这样:
修改插件项目 -> 编译生成插件 apk -> 拷贝插件 apk 到宿主 assets -> 修改宿主项目 -> 编译生成宿主 apk -> 安装宿主 apk -> 验证问题
如果每次我们修改一个很小的问题,都经历这么长的流程,那么耐心很快就耗尽了。最好是可以直接编译宿主 apk 的时候自动打包插件 apk 并拷贝到宿主 assets 目录下,这样我们不管修改什么,都直接编译宿主项目就好了。如何实现呢?还记得我们之前讲解过的 gradle 系列么?现在就是学以致用的时候了。
首先在 plugin 项目的 build.gradle 添加下面的代码:
project.afterEvaluate {
project.tasks.each {
if (it.name == "assembleDebug") {
it.doLast {
copy {
from new File(project.getBuildDir(), 'outputs/apk/debug/plugin-debug.apk').absolutePath
into new File(project.getRootProject().getProjectDir(), 'app/src/main/assets')
rename 'plugin-debug.apk', 'plugin.apk'
}
}
}
}
}
这段代码是在 afterEvaluate 的时候,遍历项目的 task,找到打包 task 也就是 assembleDebug,然后在打包之后,把生成的 apk 拷贝到宿主项目的 assets 目录下,并且重命名为 plugin.apk。 然后在 app 项目的 build.gradle 添加下面的代码:
project.afterEvaluate {
project.tasks.each {
if (it.name == 'mergeDebugAssets') {
it.dependsOn ':plugin:assembleDebug'
}
}
}
找到宿主打包的 mergeDebugAssets 任务,依赖插件项目的打包,这样每次编译宿主项目的时候,会先编译插件项目,然后拷贝插件 apk 到宿主 apk 的 assets 目录下,以后每次修改,只要编译宿主项目就可以了。
四、ClassLoader
ClassLoader 是插件化中必须要掌握的,因为插件是未安装的 apk,系统不会处理其中的类,所以需要我们自己来处理。
4.1 java 中的 ClassLoader
BootstrapClassLoader
负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等
ExtensionClassLoader
负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包
AppClassLoader
负责加载 classpath 里的 jar 包和目录
4.2 android 中的 ClassLoader
在这里,我们统称 dex 文件,包含 dex 的 apk 文件以及 jar 文件为 dex 文件 PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件
DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。
我们在插件化中一般使用的是 DexClassLoader。
4.3 双亲委派机制
每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader 的 loadClass 方法的具体实现。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
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) {
// 没有找到,再自己加载
c = findClass(name);
}
}
return c;
}
4.4 如何加载插件中的类
要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要那些参数。
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
// ...
}
}
构造函数需要四个参数:
dexPath 是需要加载的 dex / apk / jar 文件路径
optimizedDirectory 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置
librarySearchPath 是 native 依赖的位置
parent 就是父类加载器,默认会先从 parent 加载对应的类
创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:
// 从 assets 中拿出插件 apk 放到内部存储空间
private fun extractPlugin() {
var inputStream = assets.open("plugin.apk