Android插件化之RePlugin集成与使用

一.概述

通过本篇文章的学习,你将学会:
1.什么是组件化和插件化
2.RePlugin集成到自己的项目中
3.RePlugin的使用

二.组件化和插件化

组件化
组件化开发就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。
android工程的组件一般分为两种,lib组件和application组件,application组件是指该组件本身就可以运行并打包成apk,lib组件是指该组件属于app的一部分,可以供其它组件使用但是本身不能打包成apk
插件化
插件化是将一个apk根据业务功能拆分成不同的子apk(也就是不同的插件),包括一个宿主和多个插件,每个子apk可以独立编译打包,最终发布上线的是集成后的apk。在apk使用时,每个插件是动态加载的,插件也可以进行热修复和热更新。
利用插件化方案,可以让您的应用变得“小而精”。只有当用户需要使用某个特定功能时,才可以下载并开启,且可以随时卸载插件。不用去应用市场等到大包升级,用户可以随时体验到新版的应用。
组件化和插件化区别

技术 单位 实现内容 灵活性 特性 静动态
组件化 moudle 是解耦与加快编译,隔离不需要关注的部分 按加载时机切换,是作为lib,还是apk 组:组本来就是一个系统,每个组件不是真正意义上的独立模块 静态加载
插件化 apk 是解耦与加快编译,同时实现热插拔也就是热更新 加载的是apk,可以动态下载,动态更新,比组件化更灵活 插:是独立的apk,每个插件可以作为一个完全独立的apk运行,也可以和其他插件集成为大apk 动态加载,只用真正使用某个插件时,才加载该插件

插件化是基于多APK的,而组件化本质上还是只有一个 APK。组件化和插件化的最大区别(应该也是唯一区别)就是组件化在运行时不具备动态添加和修改组件的功能,但是插件化是可以的。

三.RePlugin的集成

插件化开发分为主程序和其他的插件
主程序接入
1.项目根目录添加RePlugin Host Gradle 依赖:
在项目根目录的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-host-gradle 依赖:

buildscript {
    dependencies {
        classpath 'com.qihoo360.replugin:replugin-host-gradle:2.2.4'
        ...
    }
}

2.添加 RePlugin Host Library 依赖
在 app/build.gradle 中应用 replugin-host-gradle 插件,并添加 replugin-host-lib 依赖:

// ATTENTION!!! Must be PLACED AFTER "android{}" to read the applicationId
apply plugin: 'replugin-host-gradle'

/**
 * 配置项均为可选配置,默认无需添加
 * 更多可选配置项参见replugin-host-gradle的RepluginConfig类
 * 可更改配置项参见 自动生成RePluginHostConfig.java
 */
repluginHostConfig {
    /**
     * 是否使用 AppCompat 库
     * 不需要个性化配置时,无需添加
     */
    useAppCompat = true
}

dependencies {
    compile 'com.qihoo360.replugin:replugin-host-lib:2.2.4'
    ...
}

注意:apply plugin: 'replugin-host-gradle'需要放在android{}的后面,防止出现无法读取applicationId,导致生成的坑位出现异常。
3.配置application
第一种方式:可以让我们自定义的application直接继承RePluginApplication
第二种方式:

public class MainApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        RePlugin.App.attachBaseContext(this);
        ....
    }

    @Override
    public void onCreate() {
        super.onCreate();
        RePlugin.App.onCreate();
        ....
    }
}

注意:请将RePlugin.App的调用方法,放在“仅次于super.xxx()”方法的后面,不要忘记清单文件的注册
插件接入
1.添加 RePlugin Plugin Gradle 依赖
在项目根目录的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-plugin-gradle 依赖:

buildscript {
    dependencies {
        classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.2.4'
        ...
    }
}

2.添加 RePlugin Plugin Library 依赖:

apply plugin: 'replugin-plugin-gradle'
dependencies {
    compile 'com.qihoo360.replugin:replugin-plugin-lib:2.2.4'
    ...
}

注意:apply plugin: 'replugin-host-gradle'需要放在android{}的后面,防止出现无法读取applicationId
3.在清淡文件<application>标签下添加插件别名,用于我们后面对插件的启动

<!--replugin插件别名-->
        <meta-data
            android:name="com.qihoo360.plugin.name"
            android:value="plugintest" />

注意:插件的versionCode也可以用于我们进行插件的版本升级,通过服务器获取插件的最新版本,与当前插件本本对比进行更新。

PluginInfo info = RePlugin.getPluginInfo(pluginName);
            if (info.getVersion() < serviceVersion) {//插件最新版本号由接口获得,然后进行对比,插件版本低于接口的版本就下载更新
                downPlugin(context, "http://插件地址", pluginName, activityName, true);
            }

通过以上分别对主程序和插件的接入,我们就可以进行插件化开发,以下还考虑到其安全性:
安全与签名校验
对外来的Dex和Apk做“校验”,需要:
1.打开签名校验
第一种方式:继承RePluginApplication:在创建 RePluginConfig 时调用其 setVerifySign(true) 即可。
第二种方式:非继承:需要在调用 RePlugin.App.attachBaseContext() 的地方,传递RePluginConfig,并设置setVerifySign即可:

RePluginConfig c = new RePluginConfig();
c.setVerifySign(true);
...
RePlugin.App.attachBaseContext(context, c);

2.加入合法签名
调用 RePlugin.addCertSignature() 来完成。

RePlugin.addCertSignature("379C790B7B726B51AC58E8FCBCFEB586");

其中,其参数传递的是签名证书的MD5,且去掉“:”’。

四.RePlugin的使用

插件(子apk包)我们分为内置插件和外置插件
内置插件
内置插件是指可以“随着主程序发版”而下发的插件,通常这个插件会放到主程序的Assets目录下。针对内置插件而言,开发者可无需调用安装方法,由RePlugin来“按需安装”。
添加内置插件
1.将apk包改名为:[插件名].jar
2.放入主程序的assets/plugins目录
外置插件
外置插件是指可通过“下载”、“放入SD卡”等方式获取的apk来安装并运行的插件。
安装和启动插件
启动一个插件首先我们判断插件是否安装,如果插件已经安装,则打开插件并检查插件版本更新,如果插件没有安装,则下载插件并安装插件。看我们下面的插件管理类:

public class MMCPlugin {
    private static MMCPlugin instance = null;

    private MMCPlugin() {
    }

    public static MMCPlugin getInstance() {
        if (instance == null) {
            instance = new MMCPlugin();
        }
        return instance;
    }

    /**
     * 打开插件
     *
     * @param context
     * @param pluginName
     * @param activityName
     * @param installListener
     */
    public void openPlugin(Context context, String pluginName, String activityName, InstallListener installListener) {
        this.installListener = installListener;
        if (RePlugin.isPluginInstalled(pluginName)) {//判断是否已经安装,安装了的话,就打开Activity,并且检查插件版本,需要更新的话就下载插件
            RePlugin.startActivity(context, RePlugin.createIntent(pluginName, activityName));
            if (installListener != null) {
                installListener.onSuccess();
            }
            PluginInfo info = RePlugin.getPluginInfo(pluginName);
            if (info.getVersion() < 2) {//版本号由你们接口获得,然后进行对比,插件版本低于接口的版本就下载更新
                downPlugin(context, "http://插件地址", pluginName, activityName, true);
            }
        } else {
//            downPlugin(context, "http://插件地址", pluginName, activityName, false);
            //本例子我们不进行下载直接进行安装插件
            installPlugin(context,pluginName,activityName,false);
        }
    }

    /**
     * 安装插件
     *
     * @param context
     * @param pluginName
     * @param activityName
     */
    public void installPlugin(final Context context, final String pluginName, final String activityName, boolean isUpdate) {
        final PluginInfo info = RePlugin.install(Environment.getExternalStorageDirectory() + "/" + pluginName + ".apk");
        if (info != null) {
            if (isUpdate) {//判断,是否为更新,如果是更新就预加载,下次打开就是最新的插件,不是更新就开始安装
                RePlugin.preload(info);
            } else {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        RePlugin.startActivity(context, RePlugin.createIntent(info.getName(), activityName));
                        ((Activity) context).runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                if (installListener != null) {
                                    installListener.onSuccess();
                                }
                            }
                        });
                    }
                }).start();
            }
        } else {
            if (installListener != null) {
                installListener.onFail("安装失败");
            }
        }
    }

    /**
     * 下载插件
     *
     * @param context
     * @param fileUrl
     * @param pluginName
     * @param activityName
     * @param isUpdate     是否是更新
     */
    public void downPlugin(final Context context, String fileUrl, final String pluginName, final String activityName, final boolean isUpdate) {        //获取文件存储权限
        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
        }        //下载插件,里面的下载方法可以换成你们自己的,例如okhttp,xutils3等等下载都行,然后在回调中处理那几个方法就行
        OkGo.<File>get(fileUrl).tag(context).execute(new FileCallback(Environment.getExternalStorageDirectory().getPath(), pluginName + ".apk") {
            @Override
            public void onSuccess(Response<File> response) {
                installPlugin(context, pluginName, activityName, isUpdate);
            }

            @Override
            public void downloadProgress(Progress progress) {
                super.downloadProgress(progress);
                if (installListener != null) {
                    installListener.onInstalling((int) (progress.fraction * 100));
                }
            }

            @Override
            public void onError(Response<File> response) {
                super.onError(response);
                if (installListener != null) {
                    installListener.onFail("下载失败");
                }
            }
        });
    }

    /**
     * 打开插件的Activity
     *
     * @param context
     * @param pluginName
     * @param activityName
     */
    public void openActivity(Context context, String pluginName, String activityName) {
        RePlugin.startActivity(context, RePlugin.createIntent(pluginName, activityName));
    }

    /**
     * 打开插件的Activity 可带参数传递
     *
     * @param context
     * @param intent
     * @param pluginName
     * @param activityName
     */
    public void openActivity(Context context, Intent intent, String pluginName, String activityName) {
        intent.setComponent(new ComponentName(pluginName, activityName));
        RePlugin.startActivity(context, intent);
    }

    /**
     * 打开插件的Activity 带回调
     *
     * @param activity
     * @param intent
     * @param pluginName
     * @param activityName
     * @param requestCode
     */
    public void openActivityForResult(Activity activity, Intent intent, String pluginName, String activityName, int requestCode) {
        intent.setComponent(new ComponentName(pluginName, activityName));
        RePlugin.startActivityForResult(activity, intent, requestCode, null);
    }

    private InstallListener installListener;

    public interface InstallListener {
        void onInstalling(int progress);

        void onFail(String msg);

        void onSuccess();
    }
}

在我们主程序需要打开插件的地方去调用openPlugin方法:

tv_open.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                tv_open.setClickable(false);
                MMCPlugin.getInstance().openPlugin(MainActivity.this, "plugintest", "com.linkbasic.plugintest.MainActivity", new MMCPlugin.InstallListener() {
                    @Override
                    public void onInstalling(int progress) {
                        tv_open.setText("正在下载插件:" + progress + "%");
                        if (progress == 100) {
                            tv_open.setText("插件下载完成,正在安装...");
                        }
                    }

                    @Override
                    public void onFail(String msg) {
                        tv_open.setClickable(true);
                        Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onSuccess() {
                        tv_open.setClickable(true);
                        tv_open.setText("打开插件");
                    }
                });
            }
        });

从上面我们知道,插件的安装和升级都是调用:

PluginInfo info = RePlugin.install(Environment.getExternalStorageDirectory() + "/" + pluginName + ".apk");

插件的启用调用:

 RePlugin.startActivity(context, RePlugin.createIntent(info.getName(), activityName));

其中info.getName()使我们插件的名称,activityName使我们插件中需要跳入的activity路径。
这样我们就实现了插件的安装(更新)和启动。

五.总结

以上就是关于Android插件化和RePlugin的相关知识点,如有不足或者错误的地方请在下方指正。在技术这块,我们需要多看更需要多写,我们只有不断学习,不断进步才能不被淘汰。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容