背景
近幾年Android插件化技術是比較熱門領域,6月30日滴滴也開源了自研的插件化架構VirtualAPK。一周多的時間已經有3300+star,從官方wiki可以看到,該架構功能完備,支援Android四大元件,良好的相容性,且入侵性較低,作為加載耦合插件方案是較好選擇。筆者會寫一系列VirtualAPK學習的文章,第一篇先介紹一下插件化和VirtualAPK如何接入,請先Clone https://github.com/piglet696/VirtualAPKDemo.git 項目,從一個全新的項目接入VirtualAPK。
什麼是插件化?
插件(Plugin)在計算機領域是一個通用的概念,維基百科上這樣解釋:
In computing, a plugin is a software component that adds a specific feature to an existing computer program.
意思是對現有的程式添加額外的功能元件。這樣的好處是可以對原有程式進行擴充,以Android Studio為例,我們可以安裝FindBugs插件,這樣IDE就具備了更強大的靜态代碼檢查功能。插件可以是第三方的開發者開發,隻需要遵循一定的協定即可,插件可以随時更新、解除安裝,可以看到插件化使我們的程式更加靈活。
為什麼要插件化?
插件化聽起來很美好,但是谷歌并沒有賦予Android這種能力,因為這樣存在安全風險,試想一下表面上是一個電腦App,谷歌稽核通過後,加載插件後變成一個竊取隐私的App怎麼辦。那為什麼在國内插件化技術非常熱門,各個大廠都在使用呢?總結了下有以下幾個原因:
- 國内App的版本碎片較為嚴重,想減少更新成本。國内沒有統一的應用分發市場,靜默更新需要ROM的支援,否則第三方應用市場需要Root的方式實作;
- 解決App方法數超65536問題。在谷歌官方的Multidex方案沒有出現時,可以采用插件方式解決,而現在該問題不應該是你選擇插件化的原因;
- 減少App包大小。宿主App包含了主要功能,其餘放到插件中實作,動态下發;
- 快速修改線上Bug或者釋出新功能。作為程式員這個場景肯定遇到過,剛釋出一個新版本存在Bug,再重新發版成本又特别高。而隻釋出插件成本就會很低,又能快速解決線上問題。
- 子產品解耦,協同開發。一個超級App,可以拆分成不同的插件子產品,基于一定的規則業務線之間協同開發,最終每個業務子產品生成一個插件,由宿主加載組合即可。再比如你需要接入第三方App的功能,隻需要第三方基于插件協定開發即可,不需要在宿主代碼中進行開發,很好的做到業務隔離,合作終止時直接移除插件即可。
當你想引入插件化技術時問一下自己接入的原因和要解決什麼問題,不要為了接入而接入,畢竟接入和維護需要一定的成本,比如解決一些奇怪的問題、一套插件管理&下發的背景等。
VirtualAPK架構接入
簡介
VirtualAPK的開源位址:https://github.com/didi/VirtualAPK ,請讀者詳細閱讀一下Readme和Wiki,對VirtualAPK架構有個整體的認識。引用Wiki中VirtualAPK和其他開源架構的對比:
特性 | DynamicLoadApk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
支援四大元件 | 隻支援Activity | 隻支援Activity | 隻支援Activity | 全支援 | 全支援 |
元件無需在宿主manifest中預注冊 | √ | × | √ | √ | √ |
插件可以依賴宿主 | √ | √ | √ | × | √ |
支援PendingIntent | × | × | × | √ | √ |
Android特性支援 | 大部分 | 大部分 | 大部分 | 幾乎全部 | 幾乎全部 |
相容性适配 | 一般 | 一般 | 中等 | 高 | 高 |
插件建構 | 無 | 部署aapt | Gradle插件 | 無 | Gradle插件 |
VirtualAPK工程中包括了插件的源代碼和DEMO,為了更加清晰的展示如何接入SDK,筆者建立了一個VirtualAPKDemo工程,建議Clone代碼後結合源碼閱讀文章,更容易了解。該工程包括宿主工程Host和插件工程ImageBrowser,可以把宿主工程想象成一個所有功能的展示入口,而ImageBrowser插件工程用于實作具體的圖檔浏覽業務。為了展示VirtualAPK對耦合型插件的支援,這裡宿主和插件都會依賴Picasso庫加載圖檔,但VirtualAPK架構建構插件APK時會把插件中的Picasso庫移除,插件直接使用宿主的Picasso庫,下文會具體說明。
環境準備
- Gradle版本需要為2.14.1,可以使用
檢視環境中配置的Gradle版本号。也可以使用工程中gradle -v
來編譯,可以在gradlew
中更改版本号gradle/wrapper/gradle-wrapper.properties
- com.android.tools.build的版本号為2.1.3
宿主工程接入
Host宿主工程接入需要以下6步
- 在宿主工程根目錄的build.gradle添加依賴
dependencies { classpath 'com.didi.virtualapk:gradle:0.9.0' }
- 在App的工程子產品的build.gradle添加使用gradle插件
- 添加VirtualAPK SDK compile依賴
dependencies { compile 'com.didi.virtualapk:core:0.9.0' }
- 在App的工程子產品proguard-rules.pro檔案添加混淆規則(Ps:Picasso庫的混淆規則沒有列出來)
-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.** { *; }
- MyApplication類是繼承了Application,覆寫attachBaseContext函數,進行插件SDK初始化工作
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); PluginManager.getInstance(base).init(); }
- 在使用插件之前加載插件,可以根據具體業務場景選擇合适時機加載,我是在MainActivity的onCreate時機加載
protected void onCreate(Bundle savedInstanceState) { // 加載plugin.apk插件包 PluginManager pluginManager = PluginManager.getInstance(this); File apk = new File(getExternalStorageDirectory(), "plugin.apk"); if (apk.exists()) { try { pluginManager.loadPlugin(apk); } catch (Exception e) { e.printStackTrace(); } } }
經過上述6步後,VirtualAPK插件功能就內建到宿主中了,宿主打包和運作方式沒有任何改變。接下來看下插件工程如何內建和建構的。
插件工程接入
ImageBrowser插件工程接入分為3步:
- ImageBrowser工程根目錄的build.gradle添加依賴
dependencies { classpath 'com.didi.virtualapk:gradle:0.9.0' }
- 在App的工程子產品的build.gradle添加使用gradle插件和插件配置資訊,資訊需要放在檔案最下面
解釋一下上面3個參數的作用apply plugin: 'com.didi.virtualapk.plugin' ... ... // 插件配置資訊,放在檔案最下面 virtualApk { packageId = // 插件資源id,避免資源id沖突 targetHost='../host/app' // 宿主工程的路徑 applyHostMapping = true // 插件編譯時是否啟用應用宿主的apply mapping }
- packageId用于定義每個插件的資源id,多個插件間的資源Id字首要不同,避免資源合并時産生沖突
- targetHost指明宿主工程的應用子產品,插件編譯時需要擷取宿主的一些資訊,比如mapping檔案、依賴的SDK版本資訊、R資源檔案,一定不能填錯,否則在編譯插件時會提示找不到宿主工程。
- applyHostMapping表示插件是否開啟apply mapping功能。當宿主開啟混淆時,一般情況下插件就要開啟applyHostMapping功能。因為宿主混淆後函數名可能有fun()變為a(),插件使用宿主混淆後的mapping映射來編譯插件包,這樣插件調用fun()時實際調用的是a(),才能找到正确的函數調用。
- 最後一步生成插件,需要使用Gradle指令
gradle clean assemblePlugin 或者 ./gradlew clean assemblePlugin
強調一下如果建構時確定Gradle版本需要為2.14.1,否則建構可能發生錯誤。建構成功後在build/outputs/apk 或者plugin目錄中檢視插件,plugin目錄和apk目錄中插件的差別在于plugin将插件以packageName_timestamp格式重命名,DEMO中的插件建構成功後才3KB。
插件包位置.png
官方WIKI中還說明了:
- 插件包均是Release包,不支援debug模式的插件包
- 如果存在多個productFlavors,那麼将會建構出多個插件包
前面說到VirtualAPK是對耦合型業務有很好的支援,對于我們的DEMO來說,宿主和插件都用到了Picasso庫,我們反編譯插件包後看一下裡面包含的内容如下圖所示。
插件包内容.png
可以看到,插件包中沒有Picasso庫的相關源碼,建構時VirtualAPK已經幫我們移除了。需要注意,宿主和插件包中依賴的SDK版本需要完全一緻時才會被移除。
運作插件
因為宿主中代碼寫的是從SD卡根目錄加載plugin.apk插件,是以我們需要将生成的插件重命名後放到指定位置。
adb push 插件路徑 /sdcard/plugin.apk
然後啟動宿主程式後點選"檢視更多美圖",此時加載的Activity來自于插件中,啟動代碼如下所示
@Override
public void onClick(View v) {
if (PluginManager.getInstance(this).getLoadedPlugin(PLUGIN_PKG_NAME) == null) {
Toast.makeText(getApplicationContext(),
"插件未加載,請嘗試重新開機APP", Toast.LENGTH_SHORT).show();
return;
}
Intent intent = new Intent();
intent.setClassName("com.virtualapk.imageplugin", "com.virtualapk.imageplugin.ImageBrowserActivity");
startActivity(intent);
}
OK,插件就麼運作起來了,可以看到在開發宿主和插件時,幾乎沒有侵入,在生成插件時需要一些指令操作,可以通過腳本實作編譯插件+Push到SD卡中,提高開發效率。
DEMO比較簡單隻包含了Activity,後面會擴充到其他元件的使用,大家可以仔細看下官方WIKI-插件開發指南,裡面介紹了插件中四大元件的使用、so庫的加載、已知的限制和插件加載時機的選取問題。
其他注意點
- 要想清楚宿主和插件的業務邊界很重要,才能找到插件的入口點。Demo中是以ImageBrowserActivity為邊界,從這個Activity之後的功能來來自于插件中。其實我們可以把插件看成一個類提供者,可以使用Class.forName()這種方式使用插件中的類,是以不是隻能從Activity/Fragment作為入口點。
- 編譯宿主工程時會生成一些資訊(在build/VAHost檔案夾下),插件建構時會讀取這些資訊,是以要確定運作的宿主和插件基于相同資訊建構的,宿主變化時請重新建構插件。
結束語
VirtualAPK插件架構在使用過程中還是比較簡單的,沒有過多的侵入性,是以大家可以嘗試玩起來。要實作一個插件化技術需要對Android系統原理、編譯建構都有較好了解,是以接下會學習、拆解該架構的技術原理,請大家繼續關注後續文章吧,會第一時間釋出在微信公衆号:Mob行者。VirtualAPK官方QQ群号656602897,作者玉剛大神也在啊!
微信公衆号二維碼.jpg