Android知识进阶(遥远的重头开始)

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

2019-07-21  本文已影响0人  MonkeyLei

最近完全投入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

上一篇下一篇

猜你喜欢

热点阅读