插件化框架之VirtualAPK

本文爲降低閱讀難度,並未採用“插件”這一詞,而是採用通俗易懂的“模塊”來進行闡述。本文從以下6個方面進行闡述,如有理解不對的地方,希望各位大牛不吝賜教。

  1. VirtualAPK是什麼
  2. VirtualAPK使用場景
  3. VirtualAPK如何使用
  4. VirtualAPK原理簡析
  5. VirtualAPK下載與識別
  6. 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,先整體羅列,然後分步展開:

  1. 在宿主項目,根目錄下的build.gradle文件中添加 classpath 'com.didi.virtualapk:gradle:0.9.8.6'
  2. 在宿主項目,app下的build.gradle文件中頂端添加 apply plugin: 'com.didi.virtualapk.host'
  3. 在宿主項目,app下的build.gradle文件中底部添加 compile 'com.didi.virtualapk:core:0.9.8'
  4. 在宿主項目,Application下的attachBaseContext()方法中添加 PluginManager.getInstance(base).init();
  5. 在宿主項目,app下的proguard-rules.pro文件中添加混淆規則
  6. 在模塊APK,根目錄下的build.gradle文件中添加 classpath 'com.didi.virtualapk:gradle:0.9.8.6'
  7. 在模塊APK,app下的build.gradle文件中頂端添加 apply plugin: 'com.didi.virtualapk.plugin'
  8. 在模塊APK,app下的build.gradle文件中底部配置 VirtualAPK
  9. 在宿主項目,app下的文件中加載模塊APK,然後可以跳轉模塊APK,或者與之通信
  10. 在宿主項目,app下的AndroidManifest.xml文件中添加讀寫存儲權限
  11. 構建宿主項目與模塊APK
  12. 將模塊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源碼的流程,由於本人精力有限,就不貼具體代碼了,有時間再補上。

 

可參考文檔

接入VirtualAPK時可能出現的問題及其解決辦法

VirtualAPK 源碼分析之鴻洋版本

VirtualAPK源碼分析之資源加載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章