RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案
GitHub 官方文档
RePlugin 接入项目分为 主程序接入 和 插件接入。
图片中标识的 replugin-host-* 是主程序接入需要用到的,replugin-plugin-* 是插件接入需要用的。
主程序接入
注意:Android Support库和AndroidX冲突的问题
- AS 3.2以上、Gradle 插件版本改为 4.6及以上 、compileSdkVersion 版本升级到 28及以上和buildToolsVersion 版本改为 28.0.2及以上 会引入Android新的扩展库 AndroidX。
- RePlugin 需依赖 android.support:appcompat-v7:28.*
- 目前还不知道如何兼容,所以在引入时做了一些修改
1.添加 RePlugin Host Gradle 依赖
在项目根目录的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-host-gradle 依赖:
buildscript {
dependencies {
//降低gradle插件版本
classpath 'com.android.tools.build:gradle:3.1.0'
classpath 'com.qihoo360.replugin:replugin-host-gradle:2.3.3'
...
}
}
2.添加 RePlugin Host Library 依赖
在 app/build.gradle 中应用 replugin-host-gradle 插件,并添加 replugin-host-lib 依赖:
android {
// ATTENTION!!! Must CONFIG this to accord with Gradle's standard, and avoid some error
defaultConfig {
applicationId "com.****.host"
...
}
...
}
// 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
/**
* 背景不透明的坑的数量
* 不需要个性化配置时,无需添加
*/
countNotTranslucentStandard = 6
countNotTranslucentSingleTop = 2
countNotTranslucentSingleTask = 3
countNotTranslucentSingleInstance = 2
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//不使用 androidx库
// implementation 'androidx.appcompat:appcompat:1.1.0'
// implementation 'androidx.legacy:legacy-support-v4:1.0.0'
// implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
// testImplementation 'junit:junit:4.12'
// androidTestImplementation 'androidx.test:runner:1.2.0'
// androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//引入 android.support 库
//noinspection GradleCompatible
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.qihoo360.replugin:replugin-host-lib:2.3.3'
...
}
注意
- 必须将包名写在applicatonId,而非AndroidManifest.xml中
- 请将apply plugin: 'replugin-host-gradle'放在 android{} 块之后,防止出现无法读取applicationId,导致生成的坑位出现异常
- 如果您的应用需要支持AppComat,则除了在主程序中引入AppComat-v7包以外,还需要在宿主的build.gradle中添加下面的代码:
repluginHostConfig { useAppCompat = true }
- 如果您的应用需要个性化配置坑位数量,则需要在宿主的build.gradle中添加下面的代码:
repluginHostConfig { /** * 背景不透明的坑的数量 */ countNotTranslucentStandard = 6 countNotTranslucentSingleTop = 2 countNotTranslucentSingleTask = 3 countNotTranslucentSingleInstance = 2 }
3.配置 Application 类
让工程的 Application 直接继承自 RePluginApplication。
public class MyApplication extends RePluginApplication {
}
既然声明了Application,自然还需要在AndroidManifest中配置这个Application。
<application
android:name=".MyApplication"
... />
备选:“非继承式”配置Application
若您的应用对Application类继承关系的修改有限制,或想自定义RePlugin加载过程(慎用!),则可以直接调用相关方法来使用RePlugin。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
RePlugin.App.attachBaseContext(this);
....
}
@Override
public void onCreate() {
super.onCreate();
RePlugin.App.onCreate();
....
}
@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);
....
}
}
针对“非继承式”的注意点
- 所有方法必须在UI线程来“同步”调用。切勿放到工作线程,或者通过post方法来执行
- 所有方法必须一一对应,例如 RePlugin.App.attachBaseContext 方法只在Application.attachBaseContext中调用
- 请将RePlugin.App的调用方法,放在“仅次于super.xxx()”方法的后面
插件接入
只需两步,就能让您的App变成“RePlugin插件”:
1.添加 RePlugin Plugin Gradle 依赖
在项目根目录的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-plugin-gradle 依赖:
buildscript {
dependencies {
//降低gradle插件版本
classpath 'com.android.tools.build:gradle:3.1.0'
classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.3.3'
...
}
}
2.添加 RePlugin Plugin Library 依赖
在 app/build.gradle 中应用 replugin-plugin-gradle 插件,并添加 replugin-plugin-lib 依赖:
apply plugin: 'replugin-plugin-gradle'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//不使用 androidx库
// implementation 'androidx.appcompat:appcompat:1.1.0'
// implementation 'androidx.legacy:legacy-support-v4:1.0.0'
// implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
// testImplementation 'junit:junit:4.12'
// androidTestImplementation 'androidx.test:runner:1.2.0'
// androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//引入 android.support 库
//noinspection GradleCompatible
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.qihoo360.replugin:replugin-plugin-lib:2.3.3'
...
}
3.添加插件别名、版本号
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- 别名信息需写在 activity 之前-->
<meta-data
android:name="com.qihoo360.plugin.name"
android:value="plugin2" />
<meta-data
android:name="com.qihoo360.plugin.version.ver"
android:value="100" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
项目运行测试
1、外置插件
外置插件是指可通过“下载”、“放入SD卡”等方式来安装并运行的插件。
A.安装外置插件
要安装一个外置插件,只需使用 RePlugin.install 方法,传递一个“APK路径”即可。
RePlugin.install("/sdcard/exam.apk");
- 无论安装还是升级,都会将“源文件”“移动”(而非复制)到插件的安装路径(如app_p_a)上,这样可大幅度节省安装和升级时间,但显然的,“源文件”也就会消失
- 若想改变这个行为,您可以参考RePluginConfig中的 setMoveFileWhenInstalling() 方法
- 升级插件和此等同
B.升级外置插件
升级插件的做法和“安装”是一样的,仍可以直接调用 RePlugin.install 方法。
RePlugin.install("/sdcard/exam_new.apk");
- 如果插件正在运行,则不会立即升级,而是“缓存”起来。直到所有“正在使用插件”的进程结束并重启后才会生效
- 升级可能会占用“内部存储空间”(因为要释放新的APK)
- 不支持“插件降级”,但可以“同版本覆盖”(在 RePlugin 2.1.5版本中开始支持)
安装或升级失败(返回值为Null)的原因有如下几种:
- 是否开启了“签名校验”功能且签名不在“白名单”之中?——通常在Logcat中会出现“verifySignature: invalid cert: ”。如是,则请参考“安全与签名校验”一节,了解如何将签名加白,或关闭签名校验功能(默认为关闭)
- 是否将replugin-host-lib升级到2.1.4及以上?——在2.1.3及之前版本,若没有填写“meta-data”,则可能导致安装失败,返回值为null。我们在 2.1.4 版本中已经修复了此问题(卫士和其它App的所有插件都填写了meta-data,所以问题没出现)
- APK安装包是否有问题?——请将“插件APK”直接安装到设备上(而非作为插件)试试。如果在设备中安装失败,则插件安装也一定是失败的。
- 是否没有SD卡的读写权限?——如果您的插件APK放到了SD卡上,则请务必确保主程序中拥有SD卡权限(主程序Manifest要声明,且ROM允许),否则会出现权限问题,当然,放入应用的files目录则不受影响。
- 设备内部存储空间是否不足?——通常出现此问题时其Logcat会出现“copyOrMoveApk: Copy/Move Failed”的警告。如是,则需要告知用户去清理手机。
C.卸载外置插件
要卸载插件,则需要使用 RePlugin.uninstall 方法。只需传递一个“插件名”即可。
RePlugin.uninstall("exam");
- 如果插件正在运行,则不会立即卸载插件,而是将卸载诉求记录下来。直到所有“正在使用插件”的进程结束并重启后才会生效
- 由于内置插件是捆在主程序包内的,故无法卸载“内置插件”
2、内置插件
内置插件是指可以“随着主程序发版”而下发的插件,通常这个插件会放到主程序的Assets目录下。
A.添加内置插件
添加一个内置插件是非常简单的,甚至可以“无需任何Java代码”。只需两步即可:
- 将APK改名为:[插件名].jar
- 放入主程序的assets/plugins目录这样,当编译主程序时,我们的“动态编译方案”会自动在assets目录下生成一个名叫“plugins-builtin.json”文件,记录了其内置插件的主要信息,方便运行时直接获取。
必须改成“[插件名].jar”后,才能被RePlugin-Host-Gradle识别,进而成为“内置插件”。
B.升级内置插件
内置插件的升级分为两种情况:主程序随包升级、通过install方法升级
- 主程序随包升级:当用户升级了带“新版本内置插件”的主程序时,则RePlugin会在使用插件前先做升级
- 通过install方法升级:若通过 RePlugin.install 方法做的升级(大多为用户从服务器上下载并更新),则RePlugin在调用install方法时开始做升级。当然,其规则仍遵循安装插件的规则,例如“插件运行时先不覆盖”等。值得注意的是,无论采用何种方式,均“不支持降级”,但支持“同版本覆盖”升级
C.删除内置插件
删除内置插件非常简单,直接移除相应的Jar文件,其余均交给RePlugin来自动化完成。
注意:若用户已使用了内置插件,则即便用户升级主程序,其包内已不带这个内置插件,但用户仍可继续使用它。这样可防止出现“用户升级主程序后,发现内置插件突然用不了”的情况。
D.使用内置插件的时机
不同于“外置插件”需要先调用 RePlugin.install 方法后才能使用,内置插件可无需调用此方法。而一旦插件被使用,则RePlugin会在触发相应逻辑前,为您做下列操作:
- 将内置插件释放到数据目录下(近似于调用install方法)
- 若需要加载Dex,则还会释放“优化后的Dex”到数据目录下,这可能会需要一些时间这样做的好处是,不会占用太多的“内部存储空间”,毕竟不是所有内置插件,都一定会被用到。
3.插件预加载的用法
预加载插件就是将插件的dex“提前做释放”,并将Dex缓存到内存中,这样在下次启动插件时,可无需走dex2oat过程,速度会快很多。
- 预加载当前安装的插件此为绝大多数用到的场景。直接预加载当前安装的插件即可,如果当前正在运行这个插件,则调用此方法则是无效的,毕竟当前插件已经早就被使用过了。
RePlugin.preload(pluginName);
- 预加载新安装的插件此场景主要用于“后台升级某个插件”。如果此插件“正在被使用”,则必须借助 RePlugin.install 方法的返回值(新插件的信息)来做预加载。
PluginInfo pi = RePlugin.install("/sdcard/exam_new.apk");
if (pi != null) {
RePlugin.preload(pi);
}
4.使用插件
通过插件名和类名调起
//第一个参数是插件的包名,第二个参数是插件的Activity。
Intent intent = RePlugin.createIntent(pluginName, className);
if (!RePlugin.startActivity(MainActivity.this, intent)) {
Toast.makeText(getBaseContext(), "启动失败", Toast.LENGTH_LONG).show();
}