插件化技术已经运用到很多公司的项目中,这段时间在补这方面的知识,在这里简单记录一下我使用va框架的接入过程和遇到的问题。
创建工程
我是将宿主app和插件firstPlugin创建在一个Project中的,其中app为宿主用来加载firstPlugin的apk包。
创建Project 配置proejct
- 首先确认gradle文件夹下面的gradle-wrapper.properties内gradle版本为4.4,对应的project层的build.gradle的版本为3.0.0
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
因为VA框架采用gradle构建,同时修改了一些原本的构建过程来构建插件apk,而gradle每个版本的构建几乎都有些变化,所以在使用va框架时,最好保证和官方版本的gradle版本一致。
- 在project build.gradle中做如下修改 (注释 //增加的地方 就是要增加的内容)
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
//增加
System.properties['com.android.build.gradle.overrideVersionCheck'] = 'true'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
//增加 didi va 版本
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
//增加 保证宿主apk 插件apk 和引用lib 版本相同
ext {
VERSION_COMPILE_SDK = 27
VERSION_BUILD_TOOLS = '26.0.2'
VERSION_MIN_SDK = 15
VERSION_TARGET_SDK = 25
SOURCE_COMPATIBILITY = JavaVersion.VERSION_1_8
}
task clean(type: Delete) {
delete rootProject.buildDir
}
配置宿主app
- 增加va框架依赖(这里将appcompat版本改为23也是为了和va框架中的版本一致)
dependencies {
//...
implementation 'com.android.support:appcompat-v7:23.4.0'
implementation 'com.didi.virtualapk:core:0.9.8'
}
- 在build.gradle添加apply
apply plugin: 'com.didi.virtualapk.host'
- 应用project中定义的sdk版本等信息,并声明jdk版本
android {
compileSdkVersion VERSION_COMPILE_SDK
buildToolsVersion VERSION_BUILD_TOOLS
defaultConfig {
applicationId "com.lilee.plugin.host"
minSdkVersion VERSION_MIN_SDK
targetSdkVersion VERSION_TARGET_SDK
versionName "1.0.0"
versionCode 1
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility SOURCE_COMPATIBILITY
targetCompatibility SOURCE_COMPATIBILITY
}
//...
}
- 增加宿主release打包keystore(默认混淆关闭)
signingConfigs {
release {
storeFile file("../test.keystore")
storePassword "test123"
keyAlias "test_alias"
keyPassword "test123"
}
}
buildTypes {
release {
minifyEnabled false
shrinkResources false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
在Project中创建一个 Android library (Plugin 为插件Apk)
修改插件的build.gradle,并为插件创建启动activity和launcher icon 主题等。
//apply plugin: 'com.android.library' 将library修改为applicaiton
apply plugin: 'com.android.application'
创建lanucher activity 和配置 manifest的过程比较简单,请确保plugin可以run到模拟器或者手机上就可以。
配置plugin的build.gradle
- 修改appcompat版本和宿主apk保持一致
dependencies {
implementation 'com.android.support:appcompat-v7:23.4.0'
}
- 引用project中的build.gradle中的构建版本信息
android {
compileSdkVersion VERSION_COMPILE_SDK
buildToolsVersion VERSION_BUILD_TOOLS
defaultConfig {
applicationId "com.lilee.plugin.first"
minSdkVersion VERSION_MIN_SDK
targetSdkVersion VERSION_TARGET_SDK
versionName "1.0.0"
versionCode 1
}
compileOptions {
sourceCompatibility SOURCE_COMPATIBILITY
targetCompatibility SOURCE_COMPATIBILITY
}
flavorDimensions "firstPlugin"
productFlavors {
beijing {
dimension "firstPlugin"
applicationId 'com.lilee.plugin.first'
}
shanghai {
dimension "firstPlugin"
applicationId 'com.lilee.plugin.first'
}
}
signingConfigs {
release {
storeFile file("../test.keystore")
storePassword "test123"
keyAlias "test_alias"
keyPassword "test123"
}
}
buildTypes {
debug {
minifyEnabled false
shrinkResources false
}
release {
minifyEnabled false
shrinkResources false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
}
- 在build.gradle添加
apply plugin: 'com.didi.virtualapk.plugin'
virtualApk {
packageId = 0x6f // 插件包apk的资源id(宿主apk没有的)
//... /codes/AsProjects/PluginHost/app
targetHost = '../VAPluginDemo/app' // 宿主app路径.
applyHostMapping = true // [Optional] Default value is true.
}
- 在plugin的build.gradle android{ } 添加(自己定义的资源文件全部以"a_"开头)
//只能在编码时给提示作用,并不具有约束效果。
resourcePrefix "a_"
5.在plugin下创建gradle.properties文件并在其中添加
android.useDexArchive=false
插件打包成apk
插件打包需要用gradle命令行来打包。官方指南
./gradlew clean assemblePlugin
生成的apk在 **/VAPluginDemo/firstplugin/build/outputs/apk/beijing/release/plugin-beijing-release.apk
打包中碰到的一些问题
- plugin 没有增加 gradle.properties 文件并配置android.useDexArchive=false
FAILURE: Build failed with an exception.
* What went wrong:
A problem occurred configuring project ':plugin'.
> Failed to notify project evaluation listener.
> Can't using incremental dexing mode, please add 'android.useDexArchive=false' in gradle.properties of :plugin.
> Cannot invoke method onProjectAfterEvaluate() on null object
- 要先构建一次宿主app,才可以构建plugin(因为插件构建需要宿主的mapping以及其他信息),可以尝试使用build -> build apk(s) 直接构建宿主apk。
FAILURE: Build failed with an exception.
* What went wrong:
A problem occurred configuring project ':plugin'.
> Failed to notify project evaluation listener.
> Can't find /Users/lilee/iFiles/android/codes/AsProjects/VAPlugin/app/build/VAHost/versions.txt, please check up your host application
need apply com.didi.virtualapk.host in build.gradle of host application
> Cannot invoke method onProjectAfterEvaluate() on null object
- 这个问题是因为插件中布局文件没有id。在插件主activity的布局文件中增加一个view,声明一个id。
FAILURE: Build failed with an exception.
* What went wrong:
Cannot get property 'id' on null object
- 和上面一样,要先构建一次宿主app,才可以构建plugin(因为插件构建需要宿主的mapping以及其他信息),可以尝试使用build -> build apk(s) 直接构建宿主apk。
FAILURE: Build failed with an exception.
* What went wrong:
A problem occurred configuring project ':plugin'.
> Failed to notify project evaluation listener.
> Can't find /Users/lilee/iFiles/android/codes/AsProjects/VAPlugin/app/build/VAHost/Host_R.txt, please check up your host application
need apply com.didi.virtualapk.host in build.gradle of host application
> Cannot invoke method onProjectAfterEvaluate() on null object
初始化插件
测试demo中,我是将上面生成的plugin-beijing-release.apk 放在宿主的assets中运行的。
- 在Application的onCreate()方法中初始化PluginManager
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
- 在Activity的attachBaseContext方法中读取assets中的apk,复制到硬盘中。
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
Utils.extractAssets(this, apkName);
}
- 在Activity的onCreate方法中加载插件apk
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
File apk = getFileStreamPath(apkName);
if (apk.exists()) {
try {
PluginManager.getInstance(this).loadPlugin(apk);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, e.getMessage());
}
} else {
Toast.makeText(this, "plugin apk not exists !!!", Toast.LENGTH_SHORT).show();
Log.e(TAG, "plugin apk not exists !!!");
}
// ...
}
启动插件中的四大组件
- Activity
private void startActivity() {
Intent intent = new Intent();
intent.setClassName(MainActivity.this, "com.lilee.plugin.first.MainActivity");
startActivity(intent);
}
- Service
private void startService() {
Intent intent = new Intent();
intent.setClassName(MainActivity.this, "com.lilee.plugin.first.PluginService");
startService(intent);
}
public void bindService() {
Intent intent = new Intent();
intent.setClassName(MainActivity.this, "com.lilee.plugin.first.PluginService");
bindService(intent, conn, Service.BIND_AUTO_CREATE);
}
public void unbindService() {
unbindService(conn);
}
public void stopService() {
Intent intent = new Intent();
intent.setClassName(MainActivity.this, "com.lilee.plugin.first.PluginService");
stopService(intent);
}
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "onServiceConnected");
IMyInterface a = (IMyInterface) service;
int result = a.getCount();
Log.e(TAG, String.valueOf(result));
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.d(TAG, "onServiceDisconnected");
}
};
- ContentProvider
private void cpInsert() {
String pkg = "com.lilee.plugin.first";
LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(pkg);
Uri cpUri = Uri.parse("content://com.lilee.plugin.first.Lilee");
cpUri = PluginContentResolver.wrapperUri(plugin, cpUri);
Integer count = getContentResolver().delete(cpUri, "where", null);
Toast.makeText(MainActivity.this, String.valueOf(count), Toast.LENGTH_LONG).show();
}
- BroadcastReceiver
- 静态Receiver将被动态注册,当宿主停止运行时,外部广播将无法唤醒宿主;
- 由于动态注册的缘故,插件中的Receiver必须通过隐式调用来唤起。
private void sendBroadcastReceiver() {
sendBroadcast(new Intent("plugin_receiver_one"));
}
总结
- VA的功能很强大,是一个很值得学习的框架。其实VA的github上面已经很详细的说明了接入流程。我是重新新建项目来接入的,所以开始的gradle版本和构建出了一些小问题,所以我把它记录下来。
- 最近也在看一些插件化方面的技术资料。其中修改插件apk资源id方面遇到了一些困难。而VA框架的做法很值得学习和研究。