30. 插件化实现方式-加载插件中的类

什么是插件化

插件化就是以插件下发的到本地的方式,然后通过宿主apk加载插件apk以实现相应的功能的方法,很多大型项目都采用了插件化的方式,如支付宝,微信,甚至最近在研究广点通广告sdk的时候偶然发现,连广点通广告sdk都使用了插件化开发(虽然广点通广告的主流地位已被取代,但是不排除它的技术含量是最高的)

插件化的好处

  1. 通过插件下发的方式,减小安装包体积
  2. 降低模块耦合度,提高协同开发效率
  3. 缓解方法数目可能超过65535的风险
  4. 为应用之间的互相调用提供方便

插件化与组件化

插件化开发是将整个app拆分成多个模块, 这些模块包括一个宿主和多个插件,每个模块都是一个apk,最终打包的时 候宿主apk和插件apk分开打包。
组件化开发同样是将一个app分成多个模块,每个模块都是一个组件,开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发 布的时候是将这些组件合并统一成一个apk

如何加载插件中的类

要知道怎么加载类,首先要了解类加载器,其次还要了解双亲委托机制。

PathClassLoader和DexClassLoader的区别

安卓中加载器的继承关系如上,在系统设计的初期,安卓系统考虑的是PathClassLoader用于加载正在运行中的apk,如你的app从启动到执行各种功能,它的内部都是通过PathClassLoader加载;而如果你要加载一个没有运行的apk中的类,安卓希望你使用DexClassLoader,虽然二者都是继承自BaseDexClassLoader,并且区别不大,但这样区分二者的功能也更加清晰明了。但是随着版本的更新,两者的区别逐渐被抹杀,已经到了可以相互替换的地步。在8.0(API 26)之前,它们二者的唯一区别是 第二个参数 optimizedDirectory,这个参数的意 思是生成的 odex(优化的dex)存放的路径。在8.0(API 26)及之后,二者就完全一样了

BootClassLoader

除特殊说明外,这些源码都是api28
BootClassLoader 是 PathClassLoader 的 parent,这里要注意 parent 与父类的区别。

ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
    System.out.println("classLoader = " + classLoader);
    classLoader = classLoader.getParent();
}
ClassLoader activityClassLoader = Activity.class.getClassLoader();
System.out.println("activityClassLoader = " + activityClassLoader);
类加载

从ClassLoader的loadClass方法跟下去,会找到BootClassLoader的findClass方法

    .....
    private final DexPathList pathList;
    .....
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        .....
        Class c = pathList.findClass(name, suppressedExceptions);
        .....
        return c;
    }

然后进入这里,我们只要知道,一个Element对应一个dex文件即可,因为一个app可能包含多个dex

    private Element[] dexElements
    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
        ......
        return null;
    }

可以看到class就是从这个dexElements数组中加载出来的。那么我们可以考虑下,宿主apk要调用插件apk的class,是不是可以把插件apk的dexElements和宿主的dexElements合并呢?当然可以

开始合并dexElements并加载类

a.创建插件的 DexClassLoader 类加载器,然后通过反射获取插件的 dexElements 值
b.获取宿主的 PathClassLoader 类加载器,然后通过反射获取宿主的 dexElements 值。
c.合并宿主的 dexElements 与 插件的 dexElements,生成新的 Element[]。
d.最后通过反射将新的 Element[] 赋值给宿主的 dexElements

        //3.获取BaseDexClassLoader对象,宿主的classloader
        ClassLoader classLoader = context.getClassLoader();

        //2.获取DexPathList对象,需要用到BaseDexClassLoader对象
        Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
        Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathListObj = pathListField.get(classLoader);

        //1.获取dexElements对象(宿主的),需要用到DexPathList对象
        Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
        Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        Object[] dexElementsObj = (Object[]) dexElementsField.get(pathListObj);

        //4.获取插件的dexElements对象,需要用到DexPathList对象,由上边的123部反推,DexPathList对象需要插件
        //的BaseDexClassLoader对象,那么我们需要构建一个ClassLoader去加载插件
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
                context.getCacheDir().getAbsolutePath(), null, classLoader);

        //5.插件的dexElements对象
        Object pluginPathListObj = pathListField.get(dexClassLoader);
        Object[] pluginDexElementObj = (Object[]) dexElementsField.get(pluginPathListObj);


        // 创建一个新数组
        Object[] newDexElements = (Object[]) Array.newInstance(dexElementsObj.getClass().getComponentType(),
                dexElementsObj.length + pluginDexElementObj.length);

        System.arraycopy(dexElementsObj, 0, newDexElements,
                0, dexElementsObj.length);
        System.arraycopy(pluginDexElementObj, 0, newDexElements,
                dexElementsObj.length, pluginDexElementObj.length);

        // 赋值
        dexElementsField.set(pathListObj, newDexElements);

那么接下来你就可以通过反射调用到插件中的类了

Class<?> aClass = Class.forName("com.rzm.plugin.PluginClass1");
Method print = aClass.getDeclaredMethod("print");
Object o = aClass.newInstance();
print.invoke(o);
System.out.println("MainActivity o = " + o);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章