Android外掛化——VirtualAPK外掛框架接入專案

鋸齒流沙發表於2018-02-08

由於國內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、下載完成之後,解壓:

gradle

2、新增系統環境變數

gradle

path中新增:

gradle

以上步驟就可以完成了gradle環境的配置了。

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

VirtualApk

注意:由於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

配置好外掛的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:

VirtualApk

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:

image

跳轉外掛的Activity:

image

跳轉外掛的Activity:

image

相關文章