滴滴外掛化方案 VirtualApk 完全解析(一) 初識基本用法

容華謝後發表於2018-08-01

轉載請註明出處:https://juejin.im/post/5b61076851882519f6478156

本文出自 容華謝後的部落格

1.介紹

VirtualApk GitHub地址

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,看下效果:

VirtualAPKDemo演示

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的原始碼進行分析和學習,敬請期待!

相關文章