美文网首页 Android知识进阶(遥远的重头开始)
Android-插件化技术之我也来入个门-DexClassLoa

Android-插件化技术之我也来入个门-DexClassLoa

作者: MonkeyLei | 来源:发表于2019-07-21 16:55 被阅读0次

    最近完全投入Android开发一年左右了,中间也是一直补知识。到现在,还是补了蛮多的。 布局上用约束布局很爽,应该没啥大问题。 负责的布局,rv多type用的多,另外阿里的Vlayout也有尝试,还有一些其他框架,有看过一些三方框架源码,貌似也是多布局的封装,还蛮骚的样子。自定义View之前搞过,流程基本ok,问题不会太大。然后到了后面自己封装了弹窗库,新项目也用到了(近期弹窗计划正在针对地区选择进行封装,封装后正好下一个版本迭代用上),另外Android公共组件库正在考虑中,因为做了几个项目,基本很多控件都是类似的配置,而且有些还是很重复的操作,所以打算再搞一个公共组件库(当然其中包括涉及到自定义View、方便用户配置)。简单回味下....

    然后一方面小萌新再看一些源码,一方面打算抽点时间再深入下其他方面,比如插件化、热修复等,想想还是蛮重要的勒!

    插件化的原理相关介绍:

    1. 通过DexClassLoader加载。

    2. 代理模式添加生命周期。

    3. Hook思想跳过清单验证。

    好吧,先尝试实践下DexClassLoader加载吧,参考网友的操作我们来过一下流程! 后面就开始着手做一些较深入的分析,顺便结合相关官方资料来加深印象!

    Tips: DexClassLoader.loadClass()加载后可以如下方式调用插件的方法

    //通过反射调用插件的代码

    //通过接口调用插件的代码(其中包括较为完善的面向切面编程调用插件的方法)

    **A. **试试反射的方式:

    1. 创建工程

    image

    2. 新建一个Module- plugin

    image image

    3. 然后plugin模块下新建一个被调用的方法,比如 PluginTest.java, 并提供如下操作

       public class PluginTest {
        private String feature = "不帅";
    
        public String getFeature() {
            return feature;
        }
    
        public void setFeature(String feature) {
            this.feature = feature;
        }
    }
    

    4. 然后打包这个模块为apk

    image image

    5. 将plugin下的apk拷贝到app模块下的assets目录下

    image

    6. 搞工具类将assets目录下的plugin-debug.apk拷贝到应用目录下,比如/data/user/0/popeeee.hl.com.plugin/files/Download/下,这样可以避免还需要动态申请存储权限的问题

    image

    7. 然后就可以进行拷贝操作了哟,成功后进行apk的装载

    import android.os.Environment;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    
    import popeeee.hl.com.plugin.utils.FileUtil;
    import popeeee.hl.com.plugin.utils.SystemUtils;
    
    public class MainActivity extends AppCompatActivity {
        private String pluginApkName = "plugin-debug.apk";  ///< 插件apk名称
        private String apkPath;         ///< apk存储路径
        private String apkDexPath;      ///< apk解压dex的目录、和apk存放路径为一个路径
        private DexClassLoader dexClassLoader;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            ///< 获取apk准备存储的应用本地缓存路径
            this.apkDexPath = SystemUtils.getCacheDirectory(this, Environment.DIRECTORY_DOWNLOADS).getPath();
            ///< 拷贝assets下的plugin-debug.apk到apkPath目录并获取实际路径
            this.apkPath = FileUtil.copyFilesFromAssets(this, pluginApkName, apkDexPath);
            ///< 加载apk并获取DexClassLoader对象
            this.dexClassLoader = new DexClassLoader(apkPath, apkDexPath, null, this.getClassLoader());
        }
    }
    

    8. 给当前控件添加一个点击事件,然后点击通过DexClassLoader.loadClass()加载插件对应的类,然后通过反射获取对应的方法进行调用, 之前关于反射的学习MonkeyLei:Android-自定义注解-控件注解

       /**
         * 默认hello world文本框添加点击事件 android:onClick="CallPlugin"
         * @param view
         */
        public void CallPlugin(View view) {
            try {
                ///< 加载插件的类(插件的包名.类名)
                Class<?> mClass = dexClassLoader.loadClass("popeeee.hl.com.plugin.PluginTest");
    
                ///< 获取类的实例
                Object beanObject = mClass.newInstance();
    
                ///< 然后通过反射获取对应的方法
                Method setFeatureMethod = mClass.getMethod("setFeature", String.class);
                setFeatureMethod.setAccessible(true);
                Method getFeatureMethod = mClass.getMethod("getFeature");
                getFeatureMethod.setAccessible(true);
    
                ///< 然后执行对应方法进行相关设置和获取
                setFeatureMethod.invoke(beanObject, "丑的不行呀!");
                String feature = (String) getFeatureMethod.invoke(beanObject);
    
                ///< 然后本地进行一些提示等操作
                Toast.makeText(this, feature, Toast.LENGTH_SHORT).show();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    

    9. 当点击hello world后就可以看见回调信息了呀。。。

    image

    以上方式加载过程都ok。 不过很多人都是把拷贝apk放到如下地方进行调用其实拷贝很快的,不一定要放到这里?ContextWrapper类的源码,ContextWrapper中有一个attachBaseContext()方法,这个方法会将传入的一个Context参数赋值给mBase对象,之后mBase对象就有值了。

    Application中在onCreate()方法里去初始化各种全局的变量数据是一种比较推荐的做法,但是如果你想把初始化的时间点提前到极致,也可以去重写attachBaseContext()方法,同时加载apk时进行一个简单判断:

        @Override
        protected void attachBaseContext(Context newBase) {
            super.attachBaseContext(newBase);
    
            ///< 获取apk准备存储的应用本地缓存路径
            this.apkDexPath = SystemUtils.getCacheDirectory(this, Environment.DIRECTORY_DOWNLOADS).getPath();
            ///< 拷贝assets下的plugin-debug.apk到apkPath目录并获取实际路径
            this.apkPath = FileUtil.copyFilesFromAssets(this, pluginApkName, apkDexPath);
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            ///< 判断apk是否存在
            File file = new File(apkPath);
            if (!file.exists()){
                Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
                return;
            }
            ///< 加载apk并获取DexClassLoader对象,如果有.so需要考虑第三个参数
            this.dexClassLoader = new DexClassLoader(apkPath, apkDexPath, null, this.getClassLoader());
        }
    

    B. 上面的加载方法还是略显复杂,有点麻烦了,如果加载的对象可以直接转换为PluginTest对象岂不是妙哉!

    由于app模块并没有这个PluginTeset类,所以没法这样操作,有个做法是,把插件的类复制一份到app模块,然后直接强制转换即可!试试是可以滴了....

    image

    这样也是没问题的。但是这样很麻烦呀,你想想,一旦插件要加个什么东西都需要拷贝一份,太烦了。 所以我们需要一个公共库,宿主和插件都依赖它,然后由它提供相关的实体类接口,这样只要都继承对应接口即可,维护起来也方便很多呢!

    1. 新建一个插件库(主要是与插件对应)

    image image

    2. 新建实体类对应的公共接口 PluginProvider.java

       public interface PluginProvider {
        String getFeature();
    
        void setFeature(String feature);
    }
    
    image

    3. 宿主和插件都依赖该库,修改插件实体类继承自PluginProvider

    image image
    1. 重新打包插件apk,更新到assets目录下替换之前的插件

    5. 然后宿主调用插件方式做一下改变,只需要强转为PluginProvider即可,不依赖于插件具体的实体类类型

                ///< 面向接口编程调用插件代码
                PluginProvider pluginProvider = (PluginProvider) mClass.newInstance();
                pluginProvider.setFeature("不帅么?");
    
                ///< 然后本地进行一些提示等操作
                Toast.makeText(this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
    
    image

    然后就ojbk了。

    image

    **C. **有时候我们希望通过回调的方式调用插件的方法,因为插件还要做很多事情才能回调给宿主(比如插件需要去下载皮肤主题资源,然后解压校验,成功后才能通知宿主进行相关设置),此时我们就采用接口编程回调的方式实现。回调我们经常用啦,问题不大哈...

    1. 公共插件库中我们定义一个回调接口,并提供一个invokeCallBack(ICallBack callBack)方法. IDynamic.java

    public interface IDynamic {
        void invokeCallBack(ICallBack callBack);
    }
    

    ICallBack.java

    public interface ICallBack {
        void callback(PluginProvider pluginProvider);
    }
    

    PluginProvider.java

      public interface PluginProvider {
        String getFeature();
    
        void setFeature(String feature);
    }
    

    2. 然后插件模块就可以新建一个Dynamic 继承实现IDynamic的方法,给出回调(利用线程做一个模拟)

     import popeeee.hl.com.pluginlibrary.ICallBack;
    import popeeee.hl.com.pluginlibrary.IDynamic;
    
    public class Dynamic implements IDynamic {
        @Override
        public void invokeCallBack(final ICallBack callBack) {
                        ///< 操作获取某些信息,然后回调给宿主
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    Thread.sleep(3);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                    PluginTest pluginTest = new PluginTest();
                    pluginTest.setFeature("我来自互联网,我标志了人类的一大进步!“呸,不要脸!");
                    callBack.callback(pluginTest);
                }
            }).start();
        }
    }
    

    3. 然后宿主app此时不再加载对应的实体类(因为你加载了实体类也只是自己设置,自己获取信息,没什么卵用!)。 此时我们加载Dynamic类,然后调用插件的invoke方法来请求网络等操作获取我们真实想要的数据....

    记得重新打包plugin模块的apk,更新下下

    然后修改下加载实体类并且进行强制转换

    image
       /**
         * 默认hello world文本框添加点击事件 android:onClick="CallPlugin"
         * @param view
         */
        public void CallPlugin(View view) {
            try {
                ///< 加载插件的类(插件的包名.类名)
                Class<?> mClass = dexClassLoader.loadClass("popeeee.hl.com.plugin.Dynamic");
    
                /// 1\. 反射方式调用
    //            ///< 获取类的实例
    //            Object beanObject = mClass.newInstance();
    //
    //            ///< 然后通过反射获取对应的方法
    //            Method setFeatureMethod = mClass.getMethod("setFeature", String.class);
    //            setFeatureMethod.setAccessible(true);
    //            Method getFeatureMethod = mClass.getMethod("getFeature");
    //            getFeatureMethod.setAccessible(true);
    //
    //            ///< 然后执行对应方法进行相关设置和获取
    //            setFeatureMethod.invoke(beanObject, "丑的不行呀!");
    //            String feature = (String) getFeatureMethod.invoke(beanObject);
    
    //            ///< 然后本地进行一些提示等操作
    //            Toast.makeText(this, feature, Toast.LENGTH_SHORT).show();
    
    //            /// 2\. 强制转换对应包含操作方法的对象
    //            PluginTest pluginTest = (PluginTest) mClass.newInstance();
    //            pluginTest.setFeature("丑的还可以呀2!");
    //
    //            ///< 然后本地进行一些提示等操作
    //            Toast.makeText(this, pluginTest.getFeature(), Toast.LENGTH_SHORT).show();
    
    //            ///< 面向接口编程调用插件代码
    //            PluginProvider pluginProvider = (PluginProvider) mClass.newInstance();
    //            pluginProvider.setFeature("不帅么?");
    //
    //            ///< 然后本地进行一些提示等操作
    //            Toast.makeText(this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
    
                ///< 面向切面编程调用插件代码
                IDynamic iDynamic = (IDynamic) mClass.newInstance();
                iDynamic.invokeCallBack(new ICallBack() {
                    @Override
                    public void callback(PluginProvider pluginProvider) {
                        Looper.prepare();
                        ///< 然后本地进行一些提示等操作
                        Toast.makeText(MainActivity.this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
                        Looper.loop();
                    }
                });
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    

    这样就可以了

    image

    到这里插件的入门算是有所了解,另外自己亲自实践了一把,感觉还是不一样的。另外还有插件的两个入门点,一个是插件资源的加载,一个是插件的Activity的加载启动。这个两个小萌新要后面再搞。

    搞的前提:1. 小萌新要去了解资源加载相关的机制,原理,源码的解读 2. 同样Activity的加载也是需要解读一些源码方可深入些。 另外如果对ClassLoader还在陌生的话,有必要去看下官方api,做一个解读了....

    Demo下载地址还是贴下吧,万一需要了 https://gitee.com/heyclock/doc/blob/master/PluginTest/PluginTest.zip

    先到这,贴几个我觉得不错的文章,共勉之,一起加油, 很多东西还是要自己实践...还得有自己理解!

    Android插件化技术入门

    Android插件化入门指南

    Android插件化——资源加载

    https://blog.csdn.net/liangfeng093/article/details/78120803

    相关文章

      网友评论

        本文标题:Android-插件化技术之我也来入个门-DexClassLoa

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