本篇文章主要从以下几个方面去讲解插件化的知识
一、插件化是什么
讲到插件化,肯定都知道另一个名词叫组件化,那插件化和组件化两者有什么区别呢?
组件化开发是将一个app根据业务逻辑分成多个模块进行开发,每一个模块就是一个组件,开发的过程中,我们可以单独调试这些组件,但是最终发包的时候是将这些组件合并成一个apk,这就是组件化,只有一个apk,多个library。
插件化开发同样是将一个app拆分成多个模块进行开发,但不同的是,插件化中的每一个模块就是一个单独的apk,并且最终发包的时候,宿主apk和插件apk是分开打包的。插件化项目中有一个宿主apk,可以有一个或者多个插件apk,甚至可以没有插件apk。这就类似于电脑的主板,主板是一个宿主apk,而主板上的鼠标口,键盘口是一个个的插件apk,当需要这些功能的时候,再把对应的插件apk加载进来,组装成一个完整的项目。
各大插件化对比
二、插件化的作用
试想一下,如果你的项目功能模块越来多,越来越复杂,你的app将会出现什么样的问题?
1、apk安装包的体积会越来越大
2、模块之间的耦合度非常高,协同开发的沟通成本越来越大
3、方法数目可能超过65535,app占用的内存过大
4、应用之间的相互调用
为了更好的解决上面的4个问题,就有了插件化大佬的出现,分成多个apk之后,宿主apk的大小就取决去你加载了多少个插件apk,而且没一个插件apk都可以单独开发和维护,减少了模块与模块之间的耦合度,有利于项目的维护,同时也降低了宿主apk的内存开销,每一个插件apk都有自己的一块内存。
三、插件化实现的原理
在讲解插件化原理之前,需要你已经了解了Android类加载机制和java反射机制的基础,如果还没有这些基础的,可以去看看我前面写的文章,再来看接下去的内容,会比较轻松。
我们知道插件apk里面也是包含dex文件的,根据Android类加载器,我们可以知道DexClassLoader可以直接加载dex文件或者包含dex文件的apk文件/zip文件/jar文件,所以我们就可以在宿主apk中去动态加载插件apk,加载完插件apk之后,同样会在dexElement数组中保存插件apk中的所有dex文件,那我们只要将宿主apk的dexElement数组中的dex文件和插件apk的dexElement数组中的dex文件进行合并成一个新的dexElement数组,然后重新赋值给原来宿主apk的dexElement即可。所以最关键的就是要分别获取宿主apk中的dexElement数组对象和插件apk中的dexElement数组对象,而通过源码我们可以看到dexElement对象在DexPathList中是private私有的,如果用正常手段,我们是没办法在其他地方访问这个对象的,所以我们只能采用非常规的手段(反射的技术)去获取并操作这个对象。
总结下插件化实现的整体流程
1、创建插件的DexClassLader类加载器,传入插件apk的绝对路径(插件apk由服务器下发,然后客户端保存在指定的路径),然后通过反射获取插件的dexElement值。
2、获取宿主apk的PathClassLoader类加载器(当前类的加载器),然后通过反射获取宿主的dexElement值。
3、通过反射创建一个新的数组,用来存放插件的dexElement与宿主dexElement的值。
4、通过反射将新创建的dexElement数组赋值给宿主apk的dexElements。
四、如何加载插件中的类
因为我们通过反射最终的目的是要获取dexElements成员变量的值,根据反射中获取成员变量并使用的方法
Class<?> dexPathClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathClass.getField("dexElements")
Element[] dexElements = (Element[]) dexElementsField.get("DexPathLis对象");
我们可以看出,要获取dexElements数组的值,前提是要先获取dexElementsField对象,然后还要获取DexPathList对象,因为dexElements是DexPathList类中非static的成员变量,所以外界要访问该成员变量,需要依赖DexPathList对象。接着就来获取DexPathList类的对象,我们通过源码可以发现DexPathList在BaseDexClassLoader构造函数中被创建了,也就是说DexPathList对象已经在BaseDexClassLoader的成员变量中pathList
那我们现在就要获取BaseDexClassLoader中的成员变量pathList的值,通过获取方法,此处获取DexPathList对象不能直接通过调用
Object pathList = getDeclaredConstructor(null).newInstance(null);
因为,此处相当于是重新new了一个新的DexPathList对象,而DexPathList已经在BaseDexClassLoader构造函数中创建了,所以只能 从BaseDexClassLoader类中去获取这个对象值,也就是调用以下的方法获取
Class<?> baseDexClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClass.getField("pathList");
Object dexPathList = pathListField.get("BaseDexClassLoader对象");
可以看出,要获取DexPathList对象,前提是要获取pathListField对象,然后还要获取BaseDexClassLoader对象,因为pathList是BaseDexClassLoader类中的非static的成员变量,所以要访问该变量,就需要依赖BaseDexClassLoader对象,所以现在问题就在于如何获取BaseDexClassLoader对象即可。等同于获取DexClassLoader对象。
宿主apk中获取DexClassLoader方法
因为宿主apk中的类已经被加载了,所以可以直接获取加载当前这个类的加载器
ClassLoader hostDexClassLoader = mContext.getClassLoader();
其中hostDexClassLoader就是宿主apk中BaseDexClassLoader对象。
插件apk中获取DexClassLoader方法
因为插件apk中的类还没有在宿主apk中被加载过,所以只能通过创建一个类加载
DexClassLoader pluginDexClassLoader = new DexClassLoader("dexPath", mContext.getCacheDir().getAbsolutePath(), null, hostDexClassLoader);
这样我们就分别获取到了宿主apk和插件apk中的BaseDexClassLoader对象,进而就可通过上面的流程获取两者的dexElements数组的值。
以下是完整的获取代码。
/**
* 动态加载apk,并合并插件apk的dexElement和宿主apk的dexElement
*/
private void loadApk(Context mContext) {
try {
//获取dexElementsField对象,公共的
Class<?> dexPathClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathClass.getField("dexElements");
//获取pathListField对象,公共的
Class<?> baseDexClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClass.getField("pathList");
/**
* 宿主apk
*/
//获取宿主apk加载当前类的加载器
ClassLoader hostDexClassLoader = mContext.getClassLoader();
//获取宿主apk中DexPathList对象
Object hostDexPathList = pathListField.get(hostDexClassLoader);
//获取宿主apk中的dexElements数组的值
Element[] hostDexElements = (Element[])
dexElementsField.get(hostDexPathList);
/**
* 插件apk
*/
//创建插件apk的类加载器
DexClassLoader pluginDexClassLoader = new DexClassLoader(dexPath,
mContext.getCacheDir().getAbsolutePath(), null, hostDexClassLoader);
//获取插件apk中DexPathList对象
Object pluginDexPathList = pathListField.get(pluginDexClassLoader);
//获取插件apk中的dexElements数组的值
Element[] pluginDexElements = (Element[])
dexElementsField.get(pluginDexPathList);
//合并两个dexElements数组,反射中使用数组
//创建一个新的数组
Object[] newDexElements = (Object[])
Array.newInstance(hostDexElements.getClass().getComponentType(),
hostDexElements.length + pluginDexElements.length);
//将两个dexElements数组的内容拷贝到新创建的数组中
//先拷贝宿主apk中的dexElements数组
System.arraycopy(hostDexElements,0,newDexElements,0,hostDexElements.length);
//再拷贝插件apk中的dexElements数组
System.arraycopy(pluginDexElements,0,newDexElements,
hostDexElements.length,pluginDexElements.length);
//合并完成之后,将这个新的dexElements数组赋值给宿主apk中的dexElements
dexElementsField.set(hostDexPathList,newDexElements);
//到此结束
} catch (Exception e) {
e.printStackTrace();
}
}
通过以上的代码,我们已经完成了在宿主apk中动态加载插件apk的功能,那接下来就要考虑以下几个问题:
1、如何在宿主apk中加载插件apk中的类
2、如何在宿主apk中启动插件zpk中的四大组件
3、如何在宿主apk中加载插件apk中的资源
本文我们主要来讲解第一点如何加载插件中的类
其实这个相对比较简单,我们知道要获取某一个类的话,就要通过创建构造函数获取该类的对象,但是这边的话因为宿主apk和插件apk是完全独立的,没有任何依赖,那很显然在宿主apk中是没有办法直接获取插件apk中的某一个类对象的,那怎么办呢,这时候反射技术又派上用场了,我们可以通过反射的技术去获取插件中某一个类的Class对象,进行对这个类进行操作。代码走起
/**
* 调用插件apk中PluginClass类中的function方法
*/
private void callMethod() {
try {
Class<?> pluginClass = Class.forName("com.example.pluginapp.PluginClass");
Method functionMethod = pluginClass.getDeclaredMethod("function", int.class,int.class);
Object pluginObject = pluginClass.getDeclaredConstructor(null).newInstance(null);
functionMethod.setAccessible(true);
int result = (int) functionMethod.invoke(pluginObject, 50, 100);
tv_show.setText("" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
这样我们就可以正常的在宿主apk中去调用插件apk中某一个类了。以上就是本篇文章的内容,下一篇文章将会讲解第二点,如何在宿主apk中启动插件apk中的四大组件,有兴趣的同学可以关注我的公众号"猿猴驿站",会不定期更新文章呦。