Android插件化(一)

 

本篇文章主要从以下几个方面去讲解插件化的知识

一、插件化是什么

讲到插件化,肯定都知道另一个名词叫组件化,那插件化和组件化两者有什么区别呢?

组件化开发是将一个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中的四大组件,有兴趣的同学可以关注我的公众号"猿猴驿站",会不定期更新文章呦。

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值