由于国内Android 软件的碎片化比较严重,所以衍生了Android热修复和插件化技术,而且最近这几年这两项技术都非常热门,热修复技术能够及时修复已经线上版本的bug,而插件化技术能够有效解决软件的升级成本、发布新功能和解决方法数超过65536,以及能够解耦模块等问题。
之前我已经介绍过微信的tinker热修复框架并且示范过实际项目中如何接入tinker,如果你不知道如何接入tinker,可以参考我的这篇文章《Android tinker热修复——实战接入项目》,而本文将要介绍的是滴滴的开源的插件化框架——VirtualApk
插件概念介绍
插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的平台单独运行。例如在IE中,安装相关的插件后,WEB浏览器能够直接调用插件程序,用于处理特定类型的文件。
插件化在Android中的作用在文章开头已经介绍过了,对于我的项目而已,我觉得比较大的优点就是模块解耦,组员可以协同开发,还有就是解决65k方法数的问题。
VirtualApk简介
VirtualAPK是滴滴出行自研的一款优秀的插件化框架,支持四大组件,而且不需要在宿主manifest中预注册,每个组件都有完整的生命周期,该框架具有良好的兼容性和极低的入侵性。
VirtualAPK的开源地址:https://github.com/didi/VirtualAPK/wiki
感兴趣的,可以点开源地址进去start下,在wiki里有更加详细的介绍。
gradle编译环境
如果使用VirtualAPK插件框架,需要用到gradle来编译插件,因此这里需要安装gradle环境,gradle版本需要为2.14.1。
下载地址:http://services.gradle.org/distributions/
1、下载完成之后,解压:
2、添加系统环境变量
path中添加:
以上步骤就可以完成了gradle环境的配置了。
VirtualApk接入
VirtualApk接入分为两部分,一个是宿主工程的接入,另一个是插件接入。
宿主工程接入VirtualApk
1)在项目的build.gradle添加依赖
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.3'
classpath 'com.didi.virtualapk:gradle:0.9.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
复制代码
2)宿主工程下的build.gradle添加VirtualApk依赖
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.didi.virtualapk:core:0.9.0'
}
复制代码
同时添加使用插件
apply plugin: 'com.didi.virtualapk.host'
复制代码
项目的gradle版本需要和gradle版本一直,所以需要更改项目的版本为2.14.1
注意:由于Gradle Version需要和Android Plugin Version对应,比如Android Plugin的版本为2.3的,那么Gradle Version最低需要3.3,而Android Plugin Version 2.3以下的,Gradle Version 可以为2.14.1对应。
3)在proguard-rules.pro文件添加混淆规则
-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }
-dontwarn com.didi.virtualapk.**
-dontwarn android.content.pm.**
-keep class android.** { *; }
复制代码
4)应用签名 插件包均是Release包,不支持debug模式的插件包,所以需要签名。
signingConfigs {
release {
storeFile file("../keystore/myplugin.jks")
storePassword "123456"
keyAlias "myplugin"
keyPassword "123456"
}
}
packagingOptions {
exclude 'META-INF/services/org.xmlpull.v1.XmlPullParserFactory'
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.release
}
}
复制代码
5)自定义一个类继承了Application,在attachBaseContext初始化VirtualApk。
public class BaseApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
}
复制代码
插件接入VirtualApk
1)项目的build.gradle添加依赖
dependencies {
classpath 'com.android.tools.build:gradle:2.1.3'
classpath 'com.didi.virtualapk:gradle:0.9.0'
}
复制代码
2)插件build.gradle的VirtualApk配置
apply plugin: 'com.didi.virtualapk.plugin'
virtualApk {
packageId = 0x7f // The package id of Resources.
targetHost = '../MyPlugin/app' // The path of application module in host project.
applyHostMapping = true // [Optional] Default value is true.
}
复制代码
对virtualApk的三个参数解析:
packageId:用于定义每个插件的资源id,多个插件间的资源Id前缀要不同,避免资源合并时产生冲突
targetHost:指明宿主工程的应用模块,插件编译时需要获取宿主的一些信息,比如mapping文件、依赖的SDK版本信息、R资源文件,一定不能填错,否则在编译插件时会提示找不到宿主工程。
applyHostMapping:表示插件是否开启apply mapping功能。当宿主开启混淆时,一般情况下插件就要开启applyHostMapping功能。因为宿主混淆后函数名可能有fun()变为a(),插件使用宿主混淆后的mapping映射来编译插件包,这样插件调用fun()时实际调用的是a(),才能找到正确的函数调用。
经过上面两个步骤,插件就可以使用VirtualApk了,宿舍就可以调用插件apk了。
注意
1)插件、宿主的项目gradle版本,以及编译的gradle版本要一致。
2)插件和宿主使用的VirtualApk版本要一致。
3)各个插件的virtualApk下的packageId属性值要不一致。
4) 报Failed to notify project evaluation listener错误,需要修改Gradle和build tools的版本。
5)插件和宿主的资源和文件命名不要相同。
VirtualApk使用
1)新建一个项目,根据上文的VirtualApk的配置,配置好宿主环境。
2)新建一个module
配置好插件的VirtualApk环境。
3)在适当的时机加载插件的apk。
private void loadPlugin(Context base) {
PluginManager pluginManager = PluginManager.getInstance(base);
File testplugin = new File(Environment.getExternalStorageDirectory(), "testplugin.apk");
if (testplugin.exists()) {
try {
pluginManager.loadPlugin(testplugin);
} catch (Exception e) {
e.printStackTrace();
}
} else {
Toast.makeText(getApplicationContext(),
"SDcard根目录未检测到myapp.apk插件", Toast.LENGTH_SHORT).show();
}
}
复制代码
4)调用插件的activity
findViewById(R.id.go).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (PluginManager.getInstance(MainActivity.this).getLoadedPlugin("main.plugin.com.appplugin") == null) {
Toast.makeText(getApplicationContext(),
"插件未加载,请尝试重启APP", Toast.LENGTH_SHORT).show();
return;
}
Intent intent = new Intent();
intent.setClassName("main.plugin.com.appplugin", "main.plugin.com.appplugin.HelloWorldActivity");
startActivity(intent);
}
});
复制代码
5)打包宿主apk
6)生成插件 生成插件有两种方式
a、由于在上文介绍中已经安装了gradle环境,因此可以使用Gradle命令生成插件:
gradle clean assemblePlugin
复制代码
b、使用as的Gradle:
7)打包宿主apk,安装即可使用。
调用插件说明
除了调用在项目内新建的module插件,还可以新建项目编译成的apk,也就是说VirtualApk把一切的apk看成插件加载和调用。
因此可以新建一个项目,配置好VirtualApk环境,需要注意的是:由于新建的项目是当作插件来使用的,所以配置的VirtualApk环境需要配置的是插件的VirtualApk环境。在项目的build.gradle中配置如下:
apply plugin: 'com.didi.virtualapk.plugin'
virtualApk {
packageId = 0x7f // The package id of Resources.
targetHost = '../MyPlugin/app' // The path of application module in host project.
applyHostMapping = true // [Optional] Default value is true.
}
复制代码
加载插件:
PluginManager pluginManager = PluginManager.getInstance(base);
File testplugin = new File(Environment.getExternalStorageDirectory(), "testplugin.apk");
if (testplugin.exists()) {
try {
pluginManager.loadPlugin(testplugin);
} catch (Exception e) {
e.printStackTrace();
}
} else {
Toast.makeText(getApplicationContext(),
"SDcard根目录未检测到myapp.apk插件", Toast.LENGTH_SHORT).show();
}
复制代码
调用插件的Activity
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (PluginManager.getInstance(MainActivity.this).getLoadedPlugin(MY_APP) == null) {
Toast.makeText(getApplicationContext(),"插件未加载,请尝试重启APP", Toast.LENGTH_SHORT).show();
return;
}
Intent it = new Intent();
it.setClassName(MY_APP, "com.main.myapp.PluginMainActivity");
startActivity(it);
}
});
复制代码
打包安装好宿主apk,把插件apk都放在加载的目录下就可以了。
运行截图:
宿主apk:
跳转插件的Activity:
跳转插件的Activity: