简介
近来年插件化在 Android 行业里是一个比较热门的技术。插件化可利用性很广,但事实上大多数开发者,因为未知和困难而放弃使用,所以本篇将带你了解插件化,完整的介绍如何接入滴滴出行的 VirtualAPK 插件化框架,让插件化不再是你陌生的领域。
什么是插件化
插件(Plugin)在维基百科上这样解释:
In computing, a plugin is a software component that adds a specific feature to an existing computer program.
意思是在计算机中,插件是一个软件组件,它向现有的计算机程序添加特定的功能。以 Android Studio 为例,我们可以安装 Alibaba Java Coding Guidelines 插件,这样 IDE 就具备了实时检测功能,就可以按照阿里巴巴的代码规约来进行检测,同时也能快速发现问题所在。
为什么要插件化
1.减少升级成本。国内没有统一的应用分发市场,静默升级需要ROM的支持,否则第三方应用市场需要Root的方式实现。另一方面,一开始的时候,APP 的更新都是检测到了新版本然后下载下来将旧版本覆盖安装。但是这样就带来了一个问题,如果仅仅是因为一个小 BUG 而修改了两三行代码难道也要这样升级吗。如果这个时候只发布插件不仅成本低而且又能快速解决线上问题。
2.减少App包大小。宿主 App 中只包含主要功能,其余放到插件中实现。
3.模块解耦,协同开发。一个大型 APP 可以拆分成不同的插件模块,在基于一定的规约之间协同开发。最终每个业务模块生成一个插件,由宿主加载组合即可。
VirtualAPK 框架接入
准备
VirtualAPK 的开源地址:https://github.com/didi/VirtualAPK,大家可以先读一下,以便有一个整体的认识。
由于 VirtualAPK 对 Gradle 的版本有比较特殊的要求,所以现在我们要修改一些地方。首先找到工程(宿主和插件工程)根目录下的 build.gradle 文件并打开。我们找到到如下代码并做修改:
// 修改前
classpath 'com.android.tools.build:gradle:3.0.1'
// 修改后
classpath 'com.android.tools.build:gradle:2.3.3'
由于 Gradle 在这里被我们改到 2.3.3,所以将在 app 目录下的 build.gradle 里的引用全部改为 compile。同时在此文件中特定的位置加上如下一句代码:
android {
compileSdkVersion 26
// 添加的代码
buildToolsVersion "26.0.2"
......
}
宿主工程接入
1.在宿主工程根目录下的 build.gradle 中添加依赖:
classpath 'com.didi.virtualapk:gradle:0.9.4'
2.在宿主工程 app 目录下的 build.gradle 中添加 Gradle 插件:
apply plugin: 'com.didi.virtualapk.host'
3.在宿主工程 app 目录下的 build.gradle 中添加 VirtualAPK SDK compile依赖 :
compile 'com.didi.virtualapk:core:0.9.1'
这里为了避免大家出错,我将两个 build.gradle 的代码全部贴出来。
宿主工程根目录下的 build.gradle:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'com.didi.virtualapk:gradle:0.9.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
宿主工程 app 目录下的 build.gradle:
apply plugin: 'com.android.application'
apply plugin: 'com.didi.virtualapk.host'
android {
compileSdkVersion 26
buildToolsVersion "26.0.2"
defaultConfig {
applicationId "com.app.host"
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:26.1.0'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
compile 'com.didi.virtualapk:core:0.9.1'
}
4.创建一个 MyApplication 类并继承 Application。然后将这个类配置到 AndroidManifest.xml。最后在该类中重写 attachBaseContext 函数,进行插件SDK初始化工作:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
5.在使用插件之前加载插件,可以根据具体业务逻辑选择时机加载。由于这里是演示我就直接在 MyApplication 类里的 onCreate 方法中加载:
@Override
public void onCreate() {
super.onCreate();
PluginManager pluginManager = PluginManager.getInstance(this);
//此处是当查看插件apk是否存在,如果存在就去加载(比如修改线上的bug,把插件apk下载到sdcard的根目录下取名为plug.apk)
File apk = new File(Environment.getExternalStorageDirectory(), "plug.apk");
if (apk.exists()) {
try {
Log.v(TAG, "准备加载...");
pluginManager.loadPlugin(apk);
} catch (Exception e) {
e.printStackTrace();
}
}
}
经过上述 5 步后,VirtualAPK 插件功能就集成到宿主中了,宿主打包和运行方式没有任何改变。接下来看下插件工程是如何集成和构建的。
插件工程接入
1.在插件工程根目录下的 build.gradle 中添加依赖:
classpath 'com.didi.virtualapk:gradle:0.9.4'
2.在 app 目录下的 build.gradle 中添加 Gradle 插件:
注意:Gradle 插件在宿主中是 host,在插件中则是 plugin。
apply plugin: 'com.didi.virtualapk.plugin'
3.在 app 目录下的 build.gradle 中添加插件配置信息,信息需要放在文件最下面:
注意:宿主工程的路径问题。我这里这样写是因为宿主工程和插件工程都是建在同一个文件目录下。如果不是请根据自己目录改变。
virtualApk {
packageId = 0x6f // 插件资源id,避免资源id冲突
targetHost='../Host/app' // 宿主工程的路径
applyHostMapping = true // 插件编译时是否启用应用宿主的apply mapping
}
这里我同样将两个 build.gradle 的代码全部贴出来。
插件工程根目录下的 build.gradle:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'com.didi.virtualapk:gradle:0.9.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
插件工程 app 目录下的 build.gradle:
apply plugin: 'com.android.application'
apply plugin: 'com.didi.virtualapk.plugin'
android {
compileSdkVersion 26
buildToolsVersion "26.0.0"
defaultConfig {
applicationId "com.app.plug"
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:26.1.0'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}
// 插件配置信息
virtualApk {
packageId = 0x6f // 插件资源id,避免资源id冲突
targetHost='../Host/app' // 宿主工程的路径
applyHostMapping = true // 插件编译时是否启用应用宿主的apply mapping
}
4.生成插件,需要使用 Gradle 命令。这里我们打开 cmd 进入到插件工程的 app 目录下,然后输入 gradlew clean assemblePlugin 执行。当我们看见以下图的时候就说明成功了:
成功
成功后我们在插件工程的目录下看见插件包,如下图所示:
插件包
运行插件
因为宿主中代码写的是从 SD 卡根目录加载 plug.apk 插件,所以我们需要将生成的插件重命名后放到指定位置。
然后在宿主工程的布局文件中写一个 TextView 并设置监听事件,点击后进入插件,此时加载出来的 Activity 来自于插件中,启动代码如下所示:
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.start:
if (PluginManager.getInstance(this).getLoadedPlugin("com.app.plug") == null) {
Toast.makeText(this, "plugin not loaded", Toast.LENGTH_LONG).show();
} else {
Intent intent = new Intent();
intent.setClassName("com.app.plug", "com.app.plug.PlugMainActivity");
startActivity(intent);
}
break;
}
}
到此为止,插件就这么运行起来了,而一个完整的接入流程也结束了。所以大家可以尝试玩起来,并深入研究下去。
注意点
启动插件时显示的界面与宿主相同,或者引用资源错误以及资源空指针异常。
解决方式:注意插件资源名称不要和宿主相同,否则会认为使用宿主的资源,导致插件的资源编译时被移除。我一开始定义创建 Demo 时,宿主和插件布局文件都叫 R.layout.activity_main,就导致了引用资源错误以及资源空指针异常。同时注意插件类名称也不要和宿主类名称相同,否则会出现启动插件时显示的界面与宿主相同的错误。