本文爲降低閱讀難度,並未採用“插件”這一詞,而是採用通俗易懂的“模塊”來進行闡述。本文從以下6個方面進行闡述,如有理解不對的地方,希望各位大牛不吝賜教。
- VirtualAPK是什麼
- VirtualAPK使用場景
- VirtualAPK如何使用
- VirtualAPK原理簡析
- VirtualAPK下載與識別
- VirtualAPK加載與應用
VirtualAPK是什麼
VirtualAPK是滴滴出行自研的一款優秀的插件化框架。
VirtualAPK使用場景
- 項目龐大,想分多個APK實現,但不希望佔用過多內存時——複雜性、內存
我們知道Android打開一個APK應用,就是創建了一個進程,並跑在一個單獨的虛擬機內,而創建一個進程對內存消耗是相對較大的,如果創建多個進程便會佔用更多內存。但我們又希望將項目分成多個APK,並行開發,然後多個APK之間又能相互通信。針對這樣的需求痛點,VirtualAPK是可以滿足,因爲它就是以APK作爲插件的方式嵌入到項目中,而又不會創建新的進程。
- 項目需要解耦合時——解耦合
比如,一個項目分一個宿主和多個模塊,我們希望各個模塊和宿主儘量解耦合。VirtualAPK是以獨立APK的方式,可以滿足。
- 項目中各模塊可以和宿主通信,也可以相互通信——通信
宿主和各模塊通過引用相同的aar來實現,即aar作爲一個三方中轉,來進行相互通信,並且模塊構建的時候會自動將這個aar從APK中剔除。
- 項目中的模塊可以自升級——多變性
各模塊可能頻繁更新功能或者修復bug,如果我們還採用一個Apk接入所有模塊需求的話,那麼每一個模塊的變動都需要Apk去更新,所以我們希望各模塊可以自升級。VirtualAPK加載APK的時候是去某個絕對路徑下,我們只需更改這個絕對路徑下的APK文件,就做到了模塊自升級。
VirtualAPK如何使用
按如下12個步驟可輕鬆接入VirtualAPK,先整體羅列,然後分步展開:
- 在宿主項目,根目錄下的build.gradle文件中添加 classpath 'com.didi.virtualapk:gradle:0.9.8.6'
- 在宿主項目,app下的build.gradle文件中頂端添加 apply plugin: 'com.didi.virtualapk.host'
- 在宿主項目,app下的build.gradle文件中底部添加 compile 'com.didi.virtualapk:core:0.9.8'
- 在宿主項目,Application下的attachBaseContext()方法中添加 PluginManager.getInstance(base).init();
- 在宿主項目,app下的proguard-rules.pro文件中添加混淆規則
- 在模塊APK,根目錄下的build.gradle文件中添加 classpath 'com.didi.virtualapk:gradle:0.9.8.6'
- 在模塊APK,app下的build.gradle文件中頂端添加 apply plugin: 'com.didi.virtualapk.plugin'
- 在模塊APK,app下的build.gradle文件中底部配置 VirtualAPK
- 在宿主項目,app下的文件中加載模塊APK,然後可以跳轉模塊APK,或者與之通信
- 在宿主項目,app下的AndroidManifest.xml文件中添加讀寫存儲權限
- 構建宿主項目與模塊APK
- 將模塊APK拷貝至存儲設備某個目錄,安裝運行宿主APK
(1)在宿主項目,根目錄下的build.gradle文件中添加 classpath 'com.didi.virtualapk:gradle:0.9.8.6'
dependencies {
//noinspection GradleDependency
classpath 'com.android.tools.build:gradle:3.1.3'
classpath group: 'org.tmatesoft.svnkit', name: 'svnkit', version: '1.8.11'
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
}
(2)在宿主項目,app下的build.gradle文件中頂端添加 apply plugin: 'com.didi.virtualapk.host'
apply plugin: 'com.android.application'
apply plugin: 'com.didi.virtualapk.host'
(3)在宿主項目,app下的build.gradle文件中底部添加 compile 'com.didi.virtualapk:core:0.9.8'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation(name: 'wifidiagnose-release', ext: 'aar')
implementation 'com.didi.virtualapk:core:0.9.8'
}
(4)在宿主項目,Application下的attachBaseContext()方法中添加 PluginManager.getInstance(base).init();
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
(5)在宿主項目,app下的proguard-rules.pro文件中添加混淆規則
-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }
-dontwarn com.didi.virtualapk.**
-dontwarn android.**
-keep class android.** { *; }
(6)在模塊APK,根目錄下的build.gradle文件中添加 classpath 'com.didi.virtualapk:gradle:0.9.8.6'
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
// classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')
// classpath ("com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:${TINKERPATCH_VERSION}") { changing = true }
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}
(7)在模塊APK,app下的build.gradle文件中頂端添加 apply plugin: 'com.didi.virtualapk.plugin'
apply plugin: 'com.android.application'
//apply from: 'tinkerpatch.gradle'
apply plugin: 'com.didi.virtualapk.plugin'
(8)在模塊APK,app下的build.gradle文件中底部配置VirtualAPK
virtualApk {
// 插件資源表中的packageId,需要確保不同插件有不同的packageId.
// 範圍 0x1f - 0x7f
packageId = 0x6f
// 宿主工程application模塊的路徑,插件的構建需要依賴這個路徑
targetHost = 'C:\\AndroidStudioProjects\\SystemDiagnose\\app'
//默認爲true,如果插件有引用宿主的類,那麼這個選項可以使得插件和宿主保持混淆一致
applyHostMapping = true
}
(9)在宿主項目,app下的文件中加載模塊APK,然後可以跳轉模塊APK,或者與之通信
public class TestActivity extends Activity {
private static final String TAG = TestActivity.class.getSimpleName();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG,"onCreate()");
String pluginPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/plugin.apk");
File plugin = new File(pluginPath);
try {
PluginManager.getInstance(getApplicationContext()).loadPlugin(plugin);
} catch (Exception e) {
Log.e(TAG,"error");
e.printStackTrace();
}
findViewById(R.id.iv_get_plugin).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG,"onCLick");
// Given "com.agg.application" is the package name of plugin APK,
// and there is an activity called `MainActivity`.
Intent intent = new Intent();
intent.setClassName("com.agg.application", "com.agg.application.view.activity.MainActivity");
startActivity(intent);
}
});
}
}
(10)在宿主項目,app下的AndroidManifest.xml文件中添加讀寫存儲權限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
(11)構建宿主項目與模塊APK
宿主的構建和正常apk的構建方式是相同的,可以通過Build > Generate Signed APK的方式,也可以命令:gradlew clean assembleRelease,或者如下圖方式,構建完成的apk在app > build > outputs > apk > release目錄下。
模塊APK構建可以命令:gradlew clean assemblePlugin,也可以如下圖方式,構建完成的apk在app > build > outputs > plugin > release目錄下。
(12)將模塊APK拷貝至存儲設備某個目錄,安裝運行宿主APK
存儲設備目錄在第9步中使用,至此,接入VirtualAPK大功告成。
備註:
- targetHost配置問題——targetHost可以設置絕對路徑或相對路徑,它是宿主工程application模塊的路徑,模塊APK的構建需要依賴這個路徑。
- 讀寫存儲的權限設置——6.0以上手機需要動態申請,或手動在設置中打開權限。
VirtualAPK原理簡析
基本原理
- 合併宿主和插件的ClassLoader 需要注意的是,插件中的類不可以和宿主重複
- 合併插件和宿主的資源 重設插件資源的packageId,將插件資源和宿主資源合併
- 去除插件包對宿主的引用 構建時通過Gradle插件去除插件對宿主的代碼以及資源的引用
四大組件的實現原理
- Activity 採用宿主manifest中佔坑的方式來繞過系統校驗,然後再加載真正的activity;
- Service 動態代理AMS,攔截service相關的請求,將其中轉給
Service Runtime
去處理,Service Runtime
會接管系統的所有操作; - Receiver 將插件中靜態註冊的receiver重新註冊一遍;
- ContentProvider 動態代理IContentProvider,攔截provider相關的請求,將其中轉給
Provider Runtime
去處理,Provider Runtime
會接管系統的所有操作。
整體架構圖
應用架構圖
VirtualAPK下載與識別
VirtualAPK加載與應用
先以時序圖的方式,抽離出VirtualAPK源碼的流程,由於本人精力有限,就不貼具體代碼了,有時間再補上。