插件化(一)插件化思想与类加载
最开始的起源:插件化技术最初源于免安装运行 apk 的想法。
免安装的 apk 我们称它为 插件
支持插件的 app 我们称它为 宿主
免安装的 apk 我们称它为 插件
支持插件的 app 我们称它为 宿主
插件话解决的问题
- APP的功能模块越来越多,体积越来越大
- 模块之间的耦合度高,协同开发沟通成本越来越大
- 方法数目可能超过65535,APP占用的内存过大
- 应用之间的互相调用
由于维护成本高,技术难点大,大公司一线公司用的比较多,而且兼容问题比较多,所以维护起来难点大。
插件话与组件化, 模块化的区别
组件化
开发就是将一个app分成多个模块,每个模块都是一个组件,开发的 过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发 布的时候是将这些组件合并统一成一个apk,这就是组件化开发。
再具体一些,就是 组件化分模块纵向依赖公共库,横向彼此之间没有直接依赖关系。
插件化
开发和组件化略有不同,插件化开发是将整个app拆分成多个模块, 这些模块包括一个宿主和多个插件,每个模块都是一个apk,最终打包的时 候宿主apk和插件apk分开打包。
模块化
,组件化和模块化似乎类似。但是目的不一样,模块话是业务为主,用业务划分模块,但是传统的这种做法导致多个业务关联耦合。
插件话的实现思路,面临的几个难题
- 如何加载插件的类?
- 如何启动插件的四大组件?
- 如何加载插件的资源?
可以做的功能,换肤,热修复,多开,ABTest
类声明周期简单看
我们抽象一个类Person
我们抽象一个类Car
这些都是类Class
我们的Class也是类Class
加载------> 验证 -----> 准备------> 解析
|->初始化->使用->卸载
加载阶段,虚拟机做三件事:
1.通过一个类的全限定名来获取定义此类的二 进制字节流。
2.将这个字节流所代表的静态存储结构转化为 方法区域的运行时数据结构。
3.在Java堆中生成一个代表这个类的Class对象, 作为方法区域数据的访问入口
为什么我们说反射会有一定的降低效率
- 产生大量的临时对象
- 检查可见性
- 会生成字节码 --- 没有优化
- 类型转换
ClassLoader 继承的关系
ClassLoader 继承的关系.pngPathClassLoader & DexClassLoader
在8.0(API 26)之前,它们二者的唯一区别是 第二个参数 optimizedDirectory,这个参数的意 思是生成的 odex(优化的dex)存放的路径。
在8.0(API 26)及之后,二者就完全一样了。
高版本合并了,所以区别不大了
这就是兼容问题,以后有没有,每次更新都要查看,所以说维护成本高
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
写一个测试代码:
private void printClassLoader(){
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
Log.e("zcw_plugin", "classLoader:" + classLoader);
classLoader = classLoader.getParent();
}
//pathClassLoader 和 BootClassLoader 分别加载什么类
Log.e("zcw_plugin", "Activity 的 classLoader:" + Activity.class.getClassLoader());
Log.e("zcw_plugin", "Activity 的 classLoader:" + AppCompatActivity.class.getClassLoader());
}
打印
2020-11-25 12:18:01.911 7566-7566/top.zcwfeng.plugin E/zcw_plugin: classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/top.zcwfeng.plugin-FzOugxhVZye2nIoxyC1IPg==/base.apk"],nativeLibraryDirectories=[/data/app/top.zcwfeng.plugin-FzOugxhVZye2nIoxyC1IPg==/lib/x86, /system/lib]]]
2020-11-25 12:18:01.911 7566-7566/top.zcwfeng.plugin E/zcw_plugin: classLoader:java.lang.BootClassLoader@e9e6661
2020-11-25 12:18:01.911 7566-7566/top.zcwfeng.plugin E/zcw_plugin: Activity 的 classLoader:java.lang.BootClassLoader@e9e6661
2020-11-25 12:18:01.912 7566-7566/top.zcwfeng.plugin E/zcw_plugin: Activity 的 classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/top.zcwfeng.plugin-FzOugxhVZye2nIoxyC1IPg==/base.apk"],nativeLibraryDirectories=[/data/app/top.zcwfeng.plugin-FzOugxhVZye2nIoxyC1IPg==/lib/x86, /system/lib]]]
PathClassLoader --》 parent(ClassLoader类型的对象),BootClassLoader 没有parent
PathClassLoader --- 应用的 类 -- 第三方库
BootClassLoader --- SDK的类
Activity 是SDK 而不是FrameWork,而AppCompatActivity 是依赖库中的
类似Glide 都是第三方集成的依赖。
测试加载dex
dex 的文件生成命令
dx --dex --output=output.dex input.class
dx --dex --output=test.dex top/zcwfeng/plugin/Test.class
----------
source class
package top.zcwfeng.plugin;
import android.util.Log;
public class Test {
public Test() {
}
public static void print() {
Log.e("zcw_plugin", "print:启动插件中方法");
}
}
load dex
private void testLoadDex(){
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex",
MainActivity.this.getCacheDir().getAbsolutePath(),
null,
MainActivity.this.getClassLoader());
try {
Class<?> clazz = dexClassLoader.loadClass("top.zcwfeng.plugin.Test");
Method clazzMethod = clazz.getMethod("print");
clazzMethod.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
ClassLoader.Java 核心,双亲委派
先判断是否已经加载,如果没有委派双亲去加载,如果没有加载出来那么在自己查找
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) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
作用
1.避免重复加载
2.安全考虑,不能攥改
Hook 点
查找 Hook 反射 启动插件的类
一个dexFile -> 对应一个dex文件
Element --> 对应 dexFile 而 一个APK-> 多个dex文件
Elements[] dexElements ---> 一个app的所有class文件都在dexElements 里面
关注这些类的流程
ClassLoader----DexPathList---Element----DexFile----BootClassLoader---VMClassLoader----Class
因为 宿主的MainActivity 在 宿主 的 dexElements 里面
1.获取宿主dexElements
2.获取插件dexElements
3.合并两个dexElements
4.将新的dexElements 赋值到 宿主dexElements
ps:热修复原理类似,就是更换加载顺序,把修复好的elements放在未曾修复的前面加载,就不会在加载一个错误的了
目标:dexElements -- DexPathList类的对象 -- BaseDexClassLoader的对象,类加载器
获取的是宿主的类加载器 --- 反射 dexElements 宿主
获取的是插件的类加载器 --- 反射 dexElements 插件
public
class LoadUtil {
private final static String apkPath = "/sdcard/plugin-debug.apk";
public static void load(Context context) {
/**
* 宿主dexElements = 宿主dexElements + 插件dexElements
*
* 1.获取宿主dexElements
* 2.获取插件dexElements
* 3.合并两个dexElements
* 4.将新的dexElements 赋值到 宿主dexElements
*
* 目标:dexElements -- DexPathList类的对象 -- BaseDexClassLoader的对象,类加载器
*
* 获取的是宿主的类加载器 --- 反射 dexElements 宿主
*
* 获取的是插件的类加载器 --- 反射 dexElements 插件
*/
try {
Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = clazz.getDeclaredField("pathList");// 只和类有关和对象无关
pathListField.setAccessible(true);
Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
// 宿主的类加载器
ClassLoader pathClassLoader = context.getClassLoader();
// DexPathList 类对象
Object hostPathList = pathListField.get(pathClassLoader);
// 宿主的dexElements
Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);
// plugin的类加载器
ClassLoader dexClassLoader = new DexClassLoader(apkPath,
context.getCacheDir().getAbsolutePath(),
null
, pathClassLoader);//parent 考虑适配问题,不要传null
// DexPathList 类对象
Object pluginPathList = pathListField.get(dexClassLoader);
// plugin的dexElements
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
//将新的dexElements 赋值到 宿主dexElements
// 不能直接Object[] obj = new Object[] 因为我们要把obj放到反射的elements里面去,所以不行
Object[] newDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
hostDexElements.length + pluginDexElements.length);
System.arraycopy(hostDexElements, 0, newDexElements, 0,
hostDexElements.length);
System.arraycopy(pluginDexElements, 0, newDexElements, hostDexElements.length,
pluginDexElements.length);
//赋值
dexElementsField.set(hostPathList, newDexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
加载 apk插件在application
LoadUtil.load(this);
写测试方法
private void testLoadDex(){
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex",
MainActivity.this.getCacheDir().getAbsolutePath(),
null,
MainActivity.this.getClassLoader());
try {
Class<?> clazz = dexClassLoader.loadClass("top.zcwfeng.plugin.Test");
Method clazzMethod = clazz.getMethod("print");
clazzMethod.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
各大插件的介绍和对比
我们在选择开源框架的时候,需要根据自身的需求来,如果加载的插件不需要和宿主有任何耦合,也无须和宿主进行通信,比如加载第三方 App,那么推荐使用 RePlugin,其他的情况推荐使用 VirtualApk。
特性 | DynamicAPK | dynamic-load-apk | Small | DroidPlugin | RePlugin | VirtualAPK |
---|---|---|---|---|---|---|
支持四大组件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 | 全支持 |
组件无需在宿主manifest中预注册 | × | √ | √ | √ | √ | √ |
插件可以依赖宿主 | √ | √ | √ | × | √ | √ |
支持PendingIntent | × | × | × | √ | √ | √ |
Android特性支持 | 大部分 | 大部分 | 大部分 | 几乎全部 | 几乎全部 | 几乎全部 |
兼容性适配 | 一般 | 一般 | 中等 | 高 | 高 | 高 |
插件构建 | 部署aapt | 无 | Gradle插件 | 无 | Gradle插件 | Gradle插件 |