终于完成公司的项目。空出时间回顾一下插件化。
插件化主要用来解决的问题:
1.app的模块功能越来越多,体积越来越大
2.模块之间的耦合越来越高,协同开发成本越来越大
3.方法数超过65535
4.应用之间的相互调用
但是插件化维护成本很高,因为每出一个android版本,都要去适配一下。所以一般小型企业不太会搞这个玩意。
市面上的插件化开源框架
插件化主要是,启动插件app,调用插件的类的方法,和插件的资源。
分为三类,普通类,四大组件,和资源文件。
这里先说普通类的加载。
先上效果图
这是正常的toast。
当我在sdcard目录下加入插件apk
主要通过类加载来实现上面的功能。使用反射来实现(反射在调用过程中会有一些缺点1.会产生大量的临时对象,所以为频繁的gc。2.还有检查可见性。3.当反射达到一定的数量的时候,会生成字节码,并且没有优化,所以会耗时。4.会进行一些类型转换)
类加载器的关系
PathClassLoader和DexClassLoader的区别
在8.0之前。他们二者唯一的区别就是第二参数optimizedDirectory,这个参数的含义是生成odex(优化后的dex)存放的路径
翻阅源码
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
在8.0之后就完全一样了
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
BootClassLoader和PathClassLoder的区别。我们可以自己测试一下
创建一个TestActivity,加入如下代码
public void testClassLoader(){
ClassLoader classLoader = getClassLoader();
while (classLoader!=null){
Log.e(TAG, "classLoader: "+classLoader );
classLoader = classLoader.getParent();
}
}
可以看到当前类的类加载器是PathClassLoader.
而parent是BootClassLoader。
(这里的parent并不是父类,可以想像成一个链表,parent只是Classloder类的成员变量。这个要注意)
PathClassLoader用来加载自定义以及第三方开源框架的类。
BootClassLoader用来家在sdk中的类。
就像上方TestActivity的类加载器为PathClassLoder
而TestActivity继承了Activity,所以Activity的类加载器为BootActivity
说到类加载不得不说双亲委派机制了。上源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 查询自己是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//委派给parent去查询
if (parent != null) {
//其实就是递归调用loadClass。最后调用到bootClassLoader
//bootClassLoder源码很少,大概就是查询已经加载过,然后查询自己,返回class
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) {
//如果parent没有找到,就自己加载class
c = findClass(name);
}
}
return c;
}
画个图
画的比较龊,见谅
至于为啥要设计双亲委派机制呢?
1.避免重复加载,当父类加载器已经加载了,就没有必要再加载一次
2.安全心考虑,避免核心api库随意修改
整个流程的示意图
虽然画的不是很好,但是基本上还是很清晰的。
可以从上图或者源码中得到,本身宿主的类都是保存在dexFile里,而后dexFile都是保存在Element[]中。到这里就很明白了,其实只要将插件的类也保存到上诉的Element[]中,然后生成新的element[],是不是就可以实现加载插件了?
而后的步骤用一句话说,就是通过反射 给你dexElement赋值。将原先的element[]改成宿主和插件合并的element[]就好了。后面就是反射的api调用了。
目标是获取dexElements
从源码中可以知道 dexElements不是静态的成员变量,所以要获取DexPathList类的对象。
final class DexPathList {
...
/** list of dex/resource (class path) elements */
private final Element[] dexElements;
...
}
DexPathList对象在哪儿?可以从源码中找到,BaseDexClassLoader中有pathList。但是这个pathList也不是静态的成员变量,所以又要获取BaseDexClassLoader的对象。而这个就是类加载器。
public class BaseDexClassLoader extends ClassLoader {
...
/** structured lists of path elements */
private final DexPathList pathList;
...
}
获取宿主的类加载器,得到的element[]就是宿主的。
获取插件的类加载器,得到的element[]就是插件的。
是不是非常清晰了?剩下的就是撸码了。
先做好准备工作,将插件apk放到sdcard目录下
private final static String apkPath = "/sdcard/test-debug.apk";
首先获取宿主的classLoader
// 宿主的 类加载器
ClassLoader pathClassLoader =context.getClassLoader();
// DexPathList类的对象
Object hostPathList = pathListField.get(pathClassLoader);
而后要获取elements则要通过上面分析的步骤,一步一步获取对象
首先获取BaseDexClassLoader的pathList
Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = clazz.getDeclaredField("pathList");
pathListField.setAccessible(true);
然后获取dexElements
Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
获取到dexElements之后,就可以获取elements了
// 宿主的 dexElements
Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);
同理可得到插件的elements
// 插件的 类加载器
ClassLoader dexClassLoader = new DexClassLoader(apkPath, context.getCacheDir().getAbsolutePath(),null, pathClassLoader);
// DexPathList类的对象
Object pluginPathList = pathListField.get(dexClassLoader);
// 插件的 dexElements
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
而后创建数组,然后合并赋值
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);
然后就没有然后了。完事了。
最后调用就好了。
这是普通类的加载,等空了再写四大组件和资源文件的。
希望大家喜欢