美文网首页 移动 前端 Python Android Java
插件化(一)插件化思想与类加载

插件化(一)插件化思想与类加载

作者: zcwfeng | 来源:发表于2020-11-25 17:56 被阅读0次

    最开始的起源:插件化技术最初源于免安装运行 apk 的想法。

    免安装的 apk 我们称它为 插件
    支持插件的 app 我们称它为 宿主

    免安装的 apk 我们称它为 插件
    支持插件的 app 我们称它为 宿主

    插件话解决的问题

    1. APP的功能模块越来越多,体积越来越大
    2. 模块之间的耦合度高,协同开发沟通成本越来越大
    3. 方法数目可能超过65535,APP占用的内存过大
    4. 应用之间的互相调用

    由于维护成本高,技术难点大,大公司一线公司用的比较多,而且兼容问题比较多,所以维护起来难点大。

    插件话与组件化, 模块化的区别

    组件化开发就是将一个app分成多个模块,每个模块都是一个组件,开发的 过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发 布的时候是将这些组件合并统一成一个apk,这就是组件化开发。
    再具体一些,就是 组件化分模块纵向依赖公共库,横向彼此之间没有直接依赖关系。

    插件化开发和组件化略有不同,插件化开发是将整个app拆分成多个模块, 这些模块包括一个宿主和多个插件,每个模块都是一个apk,最终打包的时 候宿主apk和插件apk分开打包。

    模块化,组件化和模块化似乎类似。但是目的不一样,模块话是业务为主,用业务划分模块,但是传统的这种做法导致多个业务关联耦合。

    插件话的实现思路,面临的几个难题

    1. 如何加载插件的类?
    2. 如何启动插件的四大组件?
    3. 如何加载插件的资源?

    可以做的功能,换肤,热修复,多开,ABTest

    类声明周期简单看

    我们抽象一个类Person
    我们抽象一个类Car
    这些都是类Class
    我们的Class也是类Class

    加载------> 验证 ----->  准备------> 解析
                                        |->初始化->使用->卸载
    

    加载阶段,虚拟机做三件事:
    1.通过一个类的全限定名来获取定义此类的二 进制字节流。
    2.将这个字节流所代表的静态存储结构转化为 方法区域的运行时数据结构。
    3.在Java堆中生成一个代表这个类的Class对象, 作为方法区域数据的访问入口

    为什么我们说反射会有一定的降低效率

    1. 产生大量的临时对象
    2. 检查可见性
    3. 会生成字节码 --- 没有优化
    4. 类型转换

    ClassLoader 继承的关系

    ClassLoader 继承的关系.png

    PathClassLoader & 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.安全考虑,不能攥改

    双亲委派.png

    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

    合并.png

    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插件

    相关文章

      网友评论

        本文标题:插件化(一)插件化思想与类加载

        本文链接:https://www.haomeiwen.com/subject/waxniktx.html