1 RePlugin 介绍

RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。

其主要优势有:

极其灵活:主程序无需升级(无需在Manifest中预埋组件),即可支持新增的四大组件,甚至全新的插件
**非常稳定:**Hook点仅有一处(ClassLoader),无任何Binder Hook!如此可做到其崩溃率仅为“万分之一”,并完美兼容市面上近乎所有的Android ROM
特性丰富:支持近乎所有在“单品”开发时的特性。包括静态Receiver、Task-Affinity坑位、自定义Theme、进程坑位、AppCompat、DataBinding等
易于集成:无论插件还是主程序,只需“数行”就能完成接入
管理成熟:拥有成熟稳定的“插件管理方案”,支持插件安装、升级、卸载、版本管理,甚至包括进程通讯、协议版本、安全校验等
数亿支撑:有360手机卫士庞大的数亿用户做支撑,三年多的残酷验证,确保App用到的方案是最稳定、最适合使用的

以上是官方的介绍,github地址如下:
https://github.com/Qihoo360/RePlugin
总之,根据网上的反馈以及14日成都参加thoughtworks 的replugin分享活动来看,Replugin将会是一个比较有潜力和完善的差价化框架。今天就来根据官方的wiki自己来接入到程序中。

Replugin的接入个人觉得分为三部分:
宿主的接入:这个主要是决定你要将那么module作为你的宿主,用于加载插件
插架的接入:这里可以新建一个module作为插件,也可以将现有的module(APK)改造为插件
插架化的使用:主要讲宿主和插件之间组件的通信,以及宿主和插件的调用等。

本篇博文不讲插件化的原理,只讲最基础的如何接入Replugin。想要了解原理的可以阅读官方的源代码及参考以下的几篇博文:
Replugin 全面解析 (1) http://www.jianshu.com/p/5994c2db1557
Replugin 全面解析 (2) http://www.jianshu.com/p/74a70dd6adc9
Replugin 全面解析 (3) http://www.jianshu.com/p/8465585b3507
Replugin 全面解析 (4) http://www.jianshu.com/p/f456f608aa92
Replugin 全面解析 (5) http://www.jianshu.com/p/fb9d40f4173c

2 宿主接入步骤

根据官方的文档,如下宿主的接入分为三个步骤:
https://github.com/Qihoo360/RePlugin/wiki/%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B

第 1 步:添加 RePlugin Host Gradle 依赖
这一步没什么好说的。
在项目根目录的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-host-gradle 依赖:

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

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

// ATTENTION!!! Must be PLACED AFTER "android{}" to read the applicationIdapply plugin: 'replugin-host-gradle'/** * 配置项均为可选配置,默认无需添加 * 更多可选配置项参见replugin-host-gradle的RepluginConfig类 * 可更改配置项参见 自动生成RePluginHostConfig.java */repluginHostConfig {    /**     * 是否使用 AppCompat 库     * 不需要个性化配置时,无需添加     */    useAppCompat = true    /**     * 背景不透明的坑的数量     * 不需要个性化配置时,无需添加     */    countNotTranslucentStandard = 6    countNotTranslucentSingleTop = 2    countNotTranslucentSingleTask = 3    countNotTranslucentSingleInstance = 2}dependencies {    compile 'com.qihoo360.replugin:replugin-host-lib:2.2.1'    ...}

需要注意的是apply plugin: ‘replugin-host-gradle’ 这一行需要放在 android{}之后。
另外一个需要注意版本的问题,目前最新的版本是2.2.1 如下,要不然下载不下来。
Android 360开源全面插件化框架RePlugin 实战_第1张图片

然后点击gradle同步,同步完成之后就可以进行下一步了

第 3 步:配置 Application 类
根据官方文档的描述可以继承RePluginApplication,也可以采用非继承式来初始化,由于我一般不太愿意采用继承式,所以就采用非继承式吧。这里我的配置如下:

/** * Email: [email protected] * Created by qiyei2015 on 2017/5/8. * Version: 1.0 * Description: */public class BaseApplication extends Application {    @Override    protected void attachBaseContext(Context base) {        super.attachBaseContext(base);        RePlugin.App.attachBaseContext(this);    }    @Override    public void onCreate() {        super.onCreate();        RePlugin.App.onCreate();        try {            SDKManager.initSDK(this);        } catch (Exception e) {            e.printStackTrace();        }        //初始化皮肤管理器        SkinManager.getInstance().init(this);    }    @Override    public void onLowMemory() {        super.onLowMemory();        /* Not need to be called if your application's minSdkVersion > = 14 */        RePlugin.App.onLowMemory();    }    @Override    public void onTrimMemory(int level) {        super.onTrimMemory(level);        /* Not need to be called if your application's minSdkVersion > = 14 */        RePlugin.App.onTrimMemory(level);    }    @Override    public void onConfigurationChanged(Configuration config) {        super.onConfigurationChanged(config);        /* Not need to be called if your application's minSdkVersion > = 14 */        RePlugin.App.onConfigurationChanged(config);    }}

多余的代码暂时不用管,这里注意事项就参照官方的来吧。

另外,如果你的宿主配置过程中有什么错误,建议先下载官方的demo进行运行,然后参考官方的demo进行配置。

3 插件接入步骤

插件部分的接入也可以按照官方的教程来。插件的接入其实很简单,不管是新开发的还是将现有的项目改造成插件化。都一般有以下几步

第 1 步:添加 RePlugin Plugin Gradle 依赖
在项目根目录的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-plugin-gradle 依赖:

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

这里我想说以下的是,如果是单独的一个工程肯定是没问题的,但是大家看我的工程目录:
Android 360开源全面插件化框架RePlugin 实战_第2张图片
EssayJokeApp 与appdemo都是Application类型的module,因此我刚刚在主工程已经添加了RePlugin Host Gradle的依赖,这里我就直接在该工程下添加RePlugin Plugin Gradle 即可。

buildscript {    repositories {        jcenter()    }    dependencies {        classpath 'com.android.tools.build:gradle:2.3.2'        classpath 'com.qihoo360.replugin:replugin-host-gradle:2.2.1'        classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.2.1'        // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files    }}

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

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

由于这里我想将appdemo module作为一个插架,因此配置如下:

apply plugin: 'com.android.application'android {    compileSdkVersion 25    buildToolsVersion "26.0.0"    defaultConfig {        applicationId "com.qiyei.appdemo"        minSdkVersion 16        targetSdkVersion 25        versionCode 1        versionName "1.0"        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"    }    buildTypes {        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }    sourceSets {        main {            jni.srcDirs = ['src/main/jni', 'src/main/cpp/']            jniLibs.srcDirs = ['libs']  //配置生成jniLibs        }    }    aaptOptions {       cruncherEnabled = false       useNewCruncher = false    }    repositories {        // 本地的libs目录        flatDir {            dirs 'libs' //aar目录        }    }    //解决多语言未翻译问题    lintOptions {        checkReleaseBuilds false        abortOnError false    }}// 这个plugin需要放在android配置之后,因为需要读取android中的配置项apply plugin: 'replugin-plugin-gradle'/*** * plugin配置 */repluginPluginConfig {    pluginName = "appdemo"    hostApplicationId = "com.qiyei.essayjoke"    hostAppLauncherActivity = "com.qiyei.essayjoke.activity.WelcomeActivity"}dependencies {    compile fileTree(include: ['*.jar'], dir: 'libs')    compile(name: 'hotfix_core-release-3.1.0', ext: 'aar')    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {        exclude group: 'com.android.support', module: 'support-annotations'    })    compile 'com.android.support:appcompat-v7:25.3.1'    compile 'com.qihoo360.replugin:replugin-plugin-lib:2.2.1'    testCompile 'junit:junit:4.12'    compile project(':Framework')    compile files('libs/alicloud-android-utils-1.0.3.jar')    compile files('libs/utdid4all-1.1.5.3_proguard.jar')}

其中repluginPluginConfig是我参考官方demo加上去的。

官方到这里就完了,但是我强烈建议你把下面一步也做了。

第3步 配置插件的别名
在插件的AndroidManifest.xml文件中配置插件的别名,方便后面的使用

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"          package="com.qiyei.appdemo">    ......    <application        android:name=".BaseApplication"        android:allowBackup="true"        android:icon="@mipmap/icon"        android:label="@string/app_name"        android:supportsRtl="true"        android:theme="@style/AppTheme">                <meta-data            android:name="com.qihoo360.plugin.name"            android:value="appdemo" />        ......    application>manifest>

android:value字段的值就是插件的别名。

这样,插件部分的接入基本完成了。下面有一步我觉得可以做一下。

4 测试插件的正确性
将接入后的插架编译成单独的apk,测试其功能是否正常,如果不正常则需要排查原因,做这一步是表明插件在安装前是正常的,防止因为插件本身的问题导致的错误。

4 使用插架

宿主接入了,插件也接入了,并且单独运行也没问题了,接下来我就看如何使用了。关于插架的使用可以参考官方文档。不过我这里要介绍一下我的一些简单使用。

1 插件的安装
Replugin的插件分为内部插件,外部插件。简单的说在编译前放入assets\plugins 下的xxx.jar就是内部插件,其他在运行期从网上下载的或者放入到其他位置,例如SD卡上的xxx.apk都是外部插件。内部插件与外部插件有以下几大不同
1 内部插件是xxx.jar(编译出apk以后,直接将文件改成xxx.jar)目录是assets\plugins,外置插件就是xxx.apk形式(内置插件进行升级之后也变为外置插件了)
2 内部插件不需要安装,直接调用。外部插件需要安装
3 内部插件默认的别名是 文件名 ,外部插件如果没有在manifest.xml中声明就是默认的包名
所以这也是为什么我刚刚让manifest.xml进行声明的原因了。
因此,对于内部插件,直接在编译前放入assets\plugins即可。后续可直接使用
对于外部插件,使用前需要调用RePlugin.install(fileName);进行显式的安装,安装代码如下:

            String pluginApk = "appdemo.apk";            String fileName = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + pluginApk;            File pluginFile = new File(fileName);            //文件不存在就返回            if (!pluginFile.exists()){                return;            }            PluginInfo info = null;            if (pluginFile.exists()) {                info = RePlugin.install(fileName);            }

我这里是直接将apk放在了sd卡根目录的,也可以从网上下载下来进行安装的。

2 宿主调用插件的组件
这里以启动一个插件的Activity为例进行介绍。
先来看我的插件中声明的Activity

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"          package="com.qiyei.appdemo">    <uses-permission android:name="android.permission.INTERNET"/>    <uses-permission android:name="android.permission.MOUNT_FORMAT_FILESYSTEMS"/>    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>    <uses-permission android:name="android.permission.VIBRATE"/>    <application        android:name=".BaseApplication"        android:allowBackup="true"        android:icon="@mipmap/icon"        android:label="@string/app_name"        android:supportsRtl="true"        android:theme="@style/AppTheme">                <meta-data            android:name="com.qihoo360.plugin.name"            android:value="appdemo" />        <meta-data            android:name="com.taobao.android.hotfix.IDSECRET"            android:value="Yours" />        <meta-data            android:name="com.taobao.android.hotfix.APPSECRET"            android:value="Yours" />        <meta-data            android:name="com.taobao.android.hotfix.RSASECRET"            android:value="Yours" />        <activity android:name=".activity.MainActivity">            <intent-filter>                <action android:name="android.intent.action.MAIN"/>                <category android:name="android.intent.category.LAUNCHER"/>            intent-filter>        activity>        <activity android:name=".activity.ColorTrackTextViewActivity">        activity>        <activity android:name=".activity.ViewPagerTestActivity">        activity>        <activity android:name=".activity.RecyclerViewTestActivity">        activity>        <activity android:name=".activity.EasyJokeMainActivity">        activity>        <activity android:name=".activity.SkinTestActivity">        activity>        <activity android:name=".activity.BannerTestActivity">        activity>        <activity android:name=".activity.ImageSelectedTestActivity">        activity>        <activity android:name=".activity.TestActivity">        activity>        <service            android:name=".service.LocalService"            android:enabled="true"            android:exported="false">        service>        <service            android:name=".service.RemoteService"            android:enabled="true"            android:exported="false"            android:process=":remote">        service>        <service            android:name=".service.JobWakeUpService"            android:enabled="true"            android:exported="false"            android:permission="android.permission.BIND_JOB_SERVICE">        service>        <service            android:name=".service.TestService"            android:enabled="true"            android:exported="false">        service>        <activity android:name=".activity.DataCenterTestActivity">        activity>        <activity android:name=".activity.BinderTestActivity">        activity>    application>manifest>

这里我在宿主中去调用插件中的com.qiyei.appdemo.activity.MainActivity。代码如下:

/** * Email: [email protected] * Created by qiyei2015 on 2017/6/24. * Version: 1.0 * Description: app的主界面 */public class HomeActivity extends BaseSkinActivity {    @ViewById(R.id.main_tab_layout)    private FrameLayout mMainTabLayout;    @ViewById(R.id.tab_list)    private RadioGroup mTabList;    @ViewById(R.id.home_rb)    private RadioButton mHomeButton;    @ViewById(R.id.find_rb)    private RadioButton mFindButton;    @ViewById(R.id.new_rb)    private RadioButton mNewButton;    @ViewById(R.id.message_rb)    private RadioButton mMessageButton;    private HomeFragment mHomeFragment;    private FindFragment mFindFragment;    private NewFragment mNewFragment;    private MessageFragment mMessageFragment;    private FragmentHelper mFragmentHelper;    private CommonTitleBar navigationBar;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        initData();        initView();    }    @Override    protected void initContentView() {        setContentView(R.layout.activity_home);    }    @Override    protected void initData() {        mContext = this;        mFragmentHelper = new FragmentHelper(getSupportFragmentManager(),R.id.main_tab_layout);        if (!RePlugin.isPluginInstalled("appdemo")){            String pluginApk = "appdemo.apk";            String fileName = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + pluginApk;            File pluginFile = new File(fileName);            //文件不存在就返回            if (!pluginFile.exists()){                return;            }            PluginInfo info = null;            if (pluginFile.exists()) {                info = RePlugin.install(fileName);            }            if (info != null){                //预先加载                RePlugin.preload(info);                ToastUtil.showLongToast("安装 appdemo 成功 " + info.getName());            }        }    }    @Override    protected void initView() {        navigationBar = new CommonTitleBar.Builder(this)                .setTitle(getString(R.string.home_page))                .setRightText(getString(R.string.test))                .setRightClickListener(new View.OnClickListener() {                    @Override                    public void onClick(View v) {                        RePlugin.startActivity(mContext, RePlugin.createIntent("appdemo",                                "com.qiyei.appdemo.activity.MainActivity"));                    }                })                .build();        SystemStatusBarUtil.statusBarTintColor(this,getResources().getColor(R.color.title_bar_bg_day));        mHomeButton.setOnClickListener(this);        mFindButton.setOnClickListener(this);        mNewButton.setOnClickListener(this);        mMessageButton.setOnClickListener(this);        mHomeFragment = new HomeFragment();        mFragmentHelper.add(mHomeFragment);        mHomeButton.setChecked(true);    }    @Override    public void onClick(View v) {        switch (v.getId()){            case R.id.home_rb:                if (mHomeFragment == null){                    mHomeFragment = new HomeFragment();                }                navigationBar.setTitle(getString(R.string.home_page));                mFragmentHelper.switchFragment(mHomeFragment);                break;            case R.id.find_rb:                if (mFindFragment == null){                    mFindFragment = new FindFragment();                }                navigationBar.setTitle(getString(R.string.find));                mFragmentHelper.switchFragment(mFindFragment);                break;            case R.id.new_rb:                if (mNewFragment == null){                    mNewFragment = new NewFragment();                }                navigationBar.setTitle(getString(R.string.fresh));                mFragmentHelper.switchFragment(mNewFragment);                break;            case R.id.message_rb:                if (mMessageFragment == null){                    mMessageFragment = new MessageFragment();                }                navigationBar.setTitle(getString(R.string.message));                mFragmentHelper.switchFragment(mMessageFragment);                break;            default:                break;        }    }    /**     * 从assets目录中复制某文件内容     *  @param  fileName assets目录下的Apk源文件路径     *  @param  newFileName 复制到/data/data/package_name/files/目录下文件名     */    private void copyAssetsFileToAppFiles(String fileName, String newFileName) {        InputStream is = null;        FileOutputStream fos = null;        int buffsize = 1024;        try {            is = new FileInputStream(fileName);            fos = this.openFileOutput(newFileName, Context.MODE_PRIVATE);            int byteCount = 0;            byte[] buffer = new byte[buffsize];            while((byteCount = is.read(buffer)) != -1) {                fos.write(buffer, 0, byteCount);            }            fos.flush();        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                is.close();                fos.close();            } catch (Exception e) {                e.printStackTrace();            }        }    }}

这里我做了插件的预加载,提供了首次的运行速度。另外其他代码忽略,只看下面即可。

RePlugin.startActivity(mContext, RePlugin.createIntent("appdemo",                                "com.qiyei.appdemo.activity.MainActivity")); 

这里的appdemo就是我指定的别名,要是是外置插件,不指定别名,就只能去按照包名“com.qiyei.appdemo”来调用了。

实测效果如下:

录制的效果不太好,有白屏的现象出现,真实的情况是没有的。另外我的机器是nexus 6p 系统是Android8.0 目测支持的还不错,没有什么大问题。

由于我的插件中集成了阿里的Sophix热修复框架,可能有一些bug,不过暂时不用管。

综合来看,Replugin还是挺有意思的,继续深入理解原理吧。
参考代码:github https://github.com/qiyei2015/EssayJoke dev分支

更多相关文章

  1. Android stutio 中怎么将XML中文件快速findById——Android Layo
  2. Android插件化框架系列之类加载器
  3. Android 指定Dex分包的Gradle插件
  4. Android ADT插件很不稳定,DDMS总是死?
  5. 详解Android 扫描条形码(Zxing插件)
  6. Android Studio 3.6.1下载插件失败
  7. 自定义Cordova插件、Ionic插件开发
  8. Android 版本及别名
  9. Android Studio 插件之 Android ButterKnife Zelezny

随机推荐

  1. 2020最新学习笔记
  2. 网站需要更换服务器应该怎么做?
  3. Intellij IDEA快捷键大全
  4. 留言板,字符串和数组方法 ----0407
  5. 前端插件:Web Uploader(上传图片)和富文本编
  6. CF卡里面的文件名目录名或卷标语法不正确
  7. 基于 Yii2 开发的多店铺商城系统,免费开源
  8. 统信UOS系统中使用QQ浏览器导入pfx/p12认
  9. I/O模型和Java NIO源码分析
  10. 统信UOS系统中设置WPS双面打印的方法