由於國內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: