轉載請註明出處:https://juejin.im/post/5b61076851882519f6478156
本文出自 容華謝後的部落格
1.介紹
VirtualAPK是滴滴在2017年6月開源的一款外掛化框架,支援Android四大元件,以及幾乎所有的Android特性,通過Gradle來構建外掛,整合與構建十分便捷,目前已經應用在 滴滴出行 App上,相容市面上幾乎所有的Android裝置。
VirtualAPK支援的Android版本:Android 4.0.3(API 15) - Android P(API P)
什麼是外掛化?外掛化的優勢在哪裡?
在開發的過程中,一個工程通常會被分為多個Module,用來區分不同的業務模組,一個主Module下面有多個業務Module,也就是我們常說的Library,釋出的時候打成一個apk,所有的邏輯都在這一個apk中,當版本更新或者某一個Module出現問題時,只能是全量更新這個apk,如果過於頻繁,使用者肯定會不爽,然後給你個差評。
外掛化的出現正好解決了這一難題,主Module不變(宿主),業務Module被分成一個個單獨的工程,不再和主Module一起打包,而是分別打包成apk(外掛),宿主啟動後,動態的去載入外掛。當某一個業務模組需要更新時,直接更新外掛apk就可以了,全程在後臺進行,不需要使用者參與操作,但這樣做對使用者有一定風險,App通過稽核後,有可能在後臺載入一些非法外掛,所以Google Play是禁止外掛化App上線的,有海外市場的專案要注意下。
在外掛化開發中,每個人負責不同的外掛模組,外掛之間完全解耦,開發完成後,再進行整合測試。一個宿主可以擁有多個外掛,一個外掛也可以為多個宿主服務。舉個栗子,同一個公司,A專案需要整合一個第三方登入模組,B專案也需要,那麼就可以把這個登入模組做成通用外掛,供兩個專案同時使用。
注意:整合外掛化框架的APP不能在Google Play釋出。
2.整合
注意:目前VirtualApk支援的gradle外掛最新版本為3.0.0,若有更新請參考官方Demo。
宿主
- 1.在專案根目錄的build.gradle檔案中加入VirtualAPK依賴:
dependencies {
classpath 'com.didi.virtualapk:gradle:0.9.8.4'
}
複製程式碼
- 2.在app根目錄的buil.gradle檔案中應用VirtualAPK host外掛:
apply plugin: 'com.didi.virtualapk.host'
複製程式碼
- 3.在app根目錄的buil.gradle檔案中引用VirtualAPK遠端庫:
dependencies {
implementation 'com.didi.virtualapk:core:0.9.6'
}
複製程式碼
- 4.在專案Application中初始化外掛:
public class VirtualAPKHostApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 初始化VirtualAPK
PluginManager.getInstance(base).init();
}
@Override
public void onCreate() {
super.onCreate();
// 載入儲存根目錄的外掛apk,實際專案中按需儲存
String pluginPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/plugin.apk");
File plugin = new File(pluginPath);
if (plugin.exists()) {
try {
PluginManager.getInstance(this).loadPlugin(plugin);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
複製程式碼
不要忘了在清單檔案中配置Application:
<application
android:name=".VirtualAPKHostApplication">
</application>
複製程式碼
- 5.呼叫外掛
com.yl.plugin是外掛工程的包名,com.yl.plugin.PluginActivity是外掛工程中的類,外掛工程的包名可以和宿主工程相同,但是相同包名下的類名不能相同,資源名稱也不能相同。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_start_plugin_activity).setOnClickListener(this);
}
@Override
public void onClick(View view) {
if (PluginManager.getInstance(this).getLoadedPlugin("com.yl.plugin") == null) {
Toast.makeText(this, "Plugin is not loaded!", Toast.LENGTH_SHORT).show();
} else {
Intent intent = new Intent();
intent.setClassName("com.yl.plugin", "com.yl.plugin.PluginActivity");
startActivity(intent);
}
}
}
複製程式碼
- 6.許可權
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
複製程式碼
- 7.混淆配置
-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }
-dontwarn com.didi.virtualapk.**
-dontwarn android.**
-keep class android.** { *; }
複製程式碼
外掛
- 1.在專案根目錄的build.gradle檔案中加入VirtualAPK依賴:
dependencies {
classpath 'com.didi.virtualapk:gradle:0.9.8.4'
}
複製程式碼
- 2.在app根目錄的buil.gradle檔案中應用VirtualAPK plugin外掛:
apply plugin: 'com.didi.virtualapk.plugin'
複製程式碼
- 3.在app根目錄的buil.gradle檔案中配置VirtualAPK:
需要在buil.gradle檔案中的最後位置進行此配置
virtualApk {
// 外掛資源表中的packageId,需要確保不同外掛有不同的packageId
// 範圍 0x1f - 0x7f
packageId = 0x6f
// 宿主工程application模組的路徑,外掛的構建需要依賴這個路徑
// targetHost可以設定絕對路徑或相對路徑
// ../VirtualAPKHostDemo/app 代表 VirtualAPKDemo/VirtualAPKHostDemo/app
targetHost = '../VirtualAPKHostDemo/app'
// 預設為true,如果外掛有引用宿主的類,那麼這個選項可以使得外掛和宿主保持混淆一致
applyHostMapping = true
}
複製程式碼
3.構建宿主與外掛
宿主
宿主的構建和正常apk的構建方式是相同的,可以通過Build > Generate Signed APK的方式,也可以通過下面的命令:
gradlew clean assembleRelease
複製程式碼
如果不想輸入命令,還可以這樣:
構建完成的apk在app > build > outputs > apk > release目錄下。
外掛
外掛採用下面的命令進行構建:
gradlew clean assemblePlugin
複製程式碼
如果不想輸入命令,還可以這樣:
構建完成的apk在app > build > outputs > plugin > release目錄下。
注意:因為assemblePlugin依賴於assembleRelease,所以外掛包均是Release包,不支援debug模式的外掛包。
到這裡,宿主和外掛就構建完成了,將外掛apk拷貝至儲存裝置根目錄,安裝執行宿主apk,看下效果:
4.外掛與宿主進行互動
外掛和宿主通過引用相同依賴庫的方式來進行互動,比如,宿主工程中引用了A庫,
dependencies {
implementation 'com.x.x.x.A'
}
複製程式碼
外掛工程中如果也需要訪問A庫中的類和資源,那麼可以在外掛工程中同樣引用A庫,這樣就可以和宿主工程共用A庫了,外掛構建的過程中會自動將A庫從apk中剔除。
以一個全域性變數舉例:
A庫中有一個全域性變數V = false,如果在外掛中將此變數設定為true,那麼在宿主中獲取到的V值則為true。
5.外掛目前暫不支援的特性
以下內容來自官方WiKi:
-
1.暫不支援Activity的一些不常用特性(比如process、configChanges等屬性),但是支援theme、launchMode和screenOrientation屬性。
-
2.overridePendingTransition(int enterAnim, int exitAnim)這種形式的轉場動畫,動畫資源不能使用外掛的(可以使用宿主或系統的)。
-
3.外掛中彈通知,需要統一處理,走宿主的邏輯,通知中的資原始檔不能使用外掛的(可以使用宿主或系統的)。
-
4.外掛的Activity中不支援動態申請許可權。
6.外掛中四大元件的已知約束
以下內容來自官方WiKi:
Activity,支援LaunchMode和theme
- 透明Activity,不能有啟動模式,並且主題中必須含有android:windowIsTranslucent屬性;
<style name="AppTheme.Transparent">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
</style>
複製程式碼
- 外掛中呼叫宿主的四大元件,請注意Intent中的包名。
VirtualAPK對Intent的處理遵循Android規範,外掛之間乃至外掛和宿主之間,包名是區分它們的唯一標識。
在下面的例子中,假如宿主的包名是"com.didi.virtualapk",然後在外掛中啟動一個宿主Activity,下面分別是錯誤和正確的示範:
// 錯誤的用法,因為此時intent中的包名是外掛的包名
Intent intent = new Intent(this, HostActivity.class);
startActivity(intent);
// 正確的用法
Intent intent = new Intent();
intent.setClassName("com.didi.virtualapk", "com.didi.virtualapk.HostActivity");
startActivity(intent);
複製程式碼
但是,如果想在外掛中去訪問外掛的四大元件,那麼就沒有任何要求了,下面的程式碼會在外掛Activity中嘗試啟動另一個外掛Activity:
// 正確的用法,因為此時intent中的包名是外掛的包名
Intent intent = new Intent(this, PluginActivity.class);
startActivity(intent);
複製程式碼
Service,支援跨程式bind service
無約束
BroadcastReceiver
-
靜態Receiver將被動態註冊,當宿主停止執行時,外部廣播將無法喚醒宿主;
-
由於動態註冊的緣故,外掛中的Receiver必須通過隱式呼叫來喚起。
ContentProvider,支援跨程式訪問ContentProvider
1)分情況,外掛呼叫自己的ContentProvider,如果需要用到call方法,那麼需要將provider的uri放到bundle中,否則呼叫不生效;
Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book");
Bundle bundle = PluginContentResolver.getBundleForCall(bookUri);
getContentResolver().call(bookUri, "testCall", null, bundle);
複製程式碼
2)外掛呼叫宿主和外部的ContentProvider,無約束;
3)宿主呼叫外掛的ContentProvider,需要將provider的uri包裝一下,通過PluginContentResolver.wrapperUri方法,如果涉及到call方法,參考1)中所描述的;
String pkg = "com.didi.virtualapk.demo";
LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(pkg);
Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book");
bookUri = PluginContentResolver.wrapperUri(plugin, bookUri);
Cursor bookCursor = getContentResolver().query(bookUri,
new String[]{"_id", "name"}, null, null, null);
複製程式碼
Fragment
推薦大家在Application啟動的時候去載入外掛,不然的話,請注意外掛的載入時機。考慮一種情況,如果在一個較晚的時機去載入外掛並且去訪問外掛中的資源,請注意當前的Context。比如在宿主Activity(MainActivity)中去載入外掛,接著在MainActivity去訪問外掛中的資源(比如Fragment),需要做一下顯示的hook,否則部分4.x的手機會出現資源找不到的情況。
String pkg = "com.didi.virtualapk.demo";
PluginUtil.hookActivityResources(MainActivity.this, pkg);
複製程式碼
so檔案的載入
為了提升效能,VirtualAPK在載入一個外掛時並不會主動去釋放外掛中的so,除非你在外掛apk的manifest中顯式地指定VA_IS_HAVE_LIB為true,如下所示:
<application
android:name=".VAApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/HostTheme">
<meta-data
android:name="VA_IS_HAVE_LIB"
android:value="true" />
...
</application>
複製程式碼
7.寫在最後
到這裡VirtualAPK的基本用法就介紹完了,如有錯誤或者遺漏的地方可以給我留言評論,謝謝!
程式碼已上傳至GitHub,歡迎Star、Fork!
GitHub地址:https://github.com/alidili/Demos/tree/master/VirtualAPKDemo
本文Demo的Apk下載地址:
宿主:https://github.com/alidili/Demos/raw/master/VirtualAPKDemo/host.apk
外掛:https://github.com/alidili/Demos/raw/master/VirtualAPKDemo/plugin.apk
後續會有系列文章對VirtualAPK的原始碼進行分析和學習,敬請期待!