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
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!