【轉】滴滴插件化方案 VirtualApk 完全解析(一) 初識基本用法

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

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

作者:容華謝後
來源:CSDN
原文:https://blog.csdn.net/kong_gu_you_lan/article/details/81324117
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

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