轉載請註明出處:https://juejin.cn/post/6844903649504673805android
本文出自 容華謝後的博客git
VirtualApk GitHub地址github
VirtualAPK是滴滴在2017年6月開源的一款插件化框架,支持Android四大組件,以及幾乎全部的Android特性,經過Gradle來構建插件,集成與構建十分便捷,目前已經應用在 滴滴出行 App上,兼容市面上幾乎全部的Android設備。bash
VirtualAPK支持的Android版本:Android 4.0.3(API 15) - Android P(API P)markdown
什麼是插件化?插件化的優點在哪裏?app
在開發的過程當中,一個工程一般會被分爲多個Module,用來區分不一樣的業務模塊,一個主Module下面有多個業務Module,也就是咱們常說的Library,發佈的時候打成一個apk,全部的邏輯都在這一個apk中,當版本更新或者某一個Module出現問題時,只能是全量更新這個apk,若是過於頻繁,用戶確定會不爽,而後給你個差評。框架
插件化的出現正好解決了這一難題,主Module不變(宿主),業務Module被分紅一個個單獨的工程,再也不和主Module一塊兒打包,而是分別打包成apk(插件),宿主啓動後,動態的去加載插件。當某一個業務模塊須要更新時,直接更新插件apk就能夠了,全程在後臺進行,不須要用戶參與操做,但這樣作對用戶有必定風險,App經過審覈後,有可能在後臺加載一些非法插件,因此Google Play是禁止插件化App上線的,有海外市場的項目要注意下。ide
在插件化開發中,每一個人負責不一樣的插件模塊,插件之間徹底解耦,開發完成後,再進行集成測試。一個宿主能夠擁有多個插件,一個插件也能夠爲多個宿主服務。舉個栗子,同一個公司,A項目須要集成一個第三方登陸模塊,B項目也須要,那麼就能夠把這個登陸模塊作成通用插件,供兩個項目同時使用。oop
注意:集成插件化框架的APP不能在Google Play發佈。post
注意:目前VirtualApk支持的gradle插件最新版本爲3.0.0,如有更新請參考官方Demo。
宿主
dependencies { classpath 'com.didi.virtualapk:gradle:0.9.8.4' } 複製代碼
apply plugin: 'com.didi.virtualapk.host' 複製代碼
dependencies { implementation 'com.didi.virtualapk:core:0.9.6' } 複製代碼
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> 複製代碼
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); } } } 複製代碼
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 複製代碼
-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }
-dontwarn com.didi.virtualapk.**
-dontwarn android.**
-keep class android.** { *; }
複製代碼
插件
dependencies { classpath 'com.didi.virtualapk:gradle:0.9.8.4' } 複製代碼
apply plugin: 'com.didi.virtualapk.plugin' 複製代碼
須要在buil.gradle文件中的最後位置進行此配置
virtualApk { // 插件資源表中的packageId,須要確保不一樣插件有不一樣的packageId // 範圍 0x1f - 0x7f packageId = 0x6f // 宿主工程application模塊的路徑,插件的構建須要依賴這個路徑 // targetHost能夠設置絕對路徑或相對路徑 // ../VirtualAPKHostDemo/app 表明 VirtualAPKDemo/VirtualAPKHostDemo/app targetHost = '../VirtualAPKHostDemo/app' // 默認爲true,若是插件有引用宿主的類,那麼這個選項可使得插件和宿主保持混淆一致 applyHostMapping = true } 複製代碼
宿主
宿主的構建和正常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,看下效果:
插件和宿主經過引用相同依賴庫的方式來進行交互,好比,宿主工程中引用了A庫,
dependencies { implementation 'com.x.x.x.A' } 複製代碼
插件工程中若是也須要訪問A庫中的類和資源,那麼能夠在插件工程中一樣引用A庫,這樣就能夠和宿主工程共用A庫了,插件構建的過程當中會自動將A庫從apk中剔除。
以一個全局變量舉例:
A庫中有一個全局變量V = false,若是在插件中將此變量設置爲true,那麼在宿主中獲取到的V值則爲true。
如下內容來自官方WiKi:
1.暫不支持Activity的一些不經常使用特性(好比process、configChanges等屬性),可是支持theme、launchMode和screenOrientation屬性。
2.overridePendingTransition(int enterAnim, int exitAnim)這種形式的轉場動畫,動畫資源不能使用插件的(可使用宿主或系統的)。
3.插件中彈通知,須要統一處理,走宿主的邏輯,通知中的資源文件不能使用插件的(可使用宿主或系統的)。
4.插件的Activity中不支持動態申請權限。
如下內容來自官方WiKi:
Activity,支持LaunchMode和theme
<style name="AppTheme.Transparent"> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsTranslucent">true</item> </style> 複製代碼
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> 複製代碼
到這裏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的源碼進行分析和學習,敬請期待!