基於Google動態化方案的組件化演進

基於Google動態化方案的組件化演進

前言

作者:kisson

國內Android動態化方案已經蓬勃發展數年之久,在React Natvie、Flutter這些跨平台方案未出現之前,類似Atlas、Replugin、DLA等Android動態化方案在業界獨領風騷。在國內動態化方案也分為兩個流派:組件化與插件化。比如Atlas自稱為組件化方案,另外諸如Replugin、DroidPlugin等稱為插件化方案。本文不具體說明組件化與插件化區別相關介紹文章已多入牛毛,這裡就不再贅述。

愛奇藝組件化Qigsaw

在項目膨脹到一定階段時,解耦工作就迫在眉睫。項目初期,我們會把網路請求、下載、存儲等核心功能庫作為Library Module,這是解耦雛形。然而當業務代碼繼續擴張後,具有獨立業務功能模塊也會慢慢被剝離出來,作為獨立的Library Module,這些被解耦出的業務模塊,我們稱之為業務組件,例如登錄、支付、分享等。當公司業務處於急速發展時期,過長的發布周期、過大的應用程序包體積等都會阻礙業務發展,因此業務組件動態化需求日益強烈,以此為契機插件化就此誕生。組件化初期是為解耦,羽化期就是動態部署。

我們將組件分為三種類型,核心組件、基礎組件、業務組件。在業務層分為業務組件和業務插件,業務插件相較於業務組件是具有動態部署能力,同時業務組件與業務插件能互相轉換,這取決於業務發展情況。當業務初期階段,以業務插件形式接入主客(一般會將插件作為獨立進程存在),好處是不增加主客包體積、不影響主客崩潰率等。當業務插件發展成熟且流量巨大,此時我們會考慮將其以業務組件的時候接入主客。畢竟業務插件的穩定性、到達率等都會存在風險。愛奇藝泡泡早期在Android是以業務插件形式接入,隨著泡泡業務成熟發展,DAU急速擴增,就將其接入主客變成業務組件。

組件解耦是一個長期且複雜的工程,為避免組件相互依賴,我們一般會開發一套組件間通信方案。目前來說,組件間通信方案有兩種形式:一種是協議型,另外一種是介面型。愛奇藝開源的Andromeda庫就是基於介面型組件間通信方案,支持跨進程和同進程。

基於前期調研與探索,我們決定基於Google提供動態化方案來做組件化Qigsaw,具有以下優勢。

  1. 0 Hook。不修改系統成員變數。
  2. 極少量私有Api訪問。
  3. 利用Google Android App Bundle打包插件完成打包工作,無需維護定製的打包插件。
  4. 天然支持業務插件和業務組件之間的無縫轉換。
  5. 國內走Qigsaw動態部署業務插件,國際版通過Play Store分發,共享開發工具、環境。

業務組件之間是不應該存在直接依賴,業務組件對外暴露應該以Activity、Service、Receiver等為主。

組件化探索

在愛奇藝組件化探索之原理篇中有詳細介動態載入組件的原理,同時在愛奇藝第一期移動技術沙龍中也提到我們如何探索及演進組件化框架。在開始設計愛奇藝自身組件化框架時,我們的核心訴求是組件能在組件化和插件化中隨時切換以應變業務發展需要,且能夠在主工程一起完成打包。

從上圖中打包流程中可以看出:

  1. 所有業務組件、業務插件的Manifest文件會合併。
  2. 業務插件打包產物為APK文件,用於動態部署。

眾所周知,Android四大組件必須在應用程序Manifest文件中聲明才能被正常啟動。將插件的Manifest預先聲明至主客中,我們就無需通過黑科技手段啟動四大組件,穩定性更高。但該方案無法滿足業務插件新增四大組件需求,本文後續將會介紹一種新增Activity的方案,畢竟新增Activity需求遠遠大於其他Android組件。

國內Android動態化方案不勝枚舉,其中我們選取Atlas調研,此外針對Google動態化方案Instant Apps和Android App Bundles(AAB)陸續展開分析。在年初開始組件化探索之時,Atlas的方案是比較符合我們需求,但其存在兩個比較棘手問題。

  1. 打包插件極其厚重。
  2. 存在大量私有API訪問,兼容性處理邏輯較多。

Atlas打包方案是完全自研一套,數十萬行代碼。即使套用Atlas打包插件,其接入維護成本也是巨大,而且Android Gralde插件升級後,還需等Atlas團隊適配。Atlas還大量修改aapt源碼(非aapt2),這也需投入巨大資源完成升級適配工作。

藉助Atlas打包插件或者自研一套打包方案在年初愛奇藝組件化框架立項時就被否決。因為不管哪種方式,都需要花費大量資資源,對於我們這種比較精小的團隊來說不划算。所以我們另闢蹊徑,看能不能從官方提供的動態化框架中尋找蛛絲馬跡。

Instant Apps摸索

Google於2016年推出Instant Apps,在安裝有google play service的Android設備上,只需一個鏈接,無須安裝App就可以體驗該App的部分功能。

在Instant Apps文檔中,有介紹如何開始Instant Apps開發。

上圖就是依據官方文檔介紹、工程結構、運行結果總結得出。Instant Apps提供兩種類型Gradle打包插件com.android.feature、com.android.instantapp,在com.android.feature中必須聲明一base feature模塊。所有業務feature所需公共模塊都可放入base feature中。com.android.instantapp插件作用是生成Instant App所需應用程序包,通過圖中輸出產物可知,它是zip格式壓縮文件,通過解壓發現它包含所有feature插件生成的apk文件。免安裝運行apk,以DroidPlugin為代表的插件化方案也能如此。所以,我們可以大膽猜測Instant Apps就是官方插件化機制。

Instant Apps實踐

上圖是運行Android Studio中Instant Apps工程後在Nexus 5(OS 6.0)得到的啟動頁。在該頁有兩種操作方式,一種是打開Instant App,另外一種是用瀏覽器打開該頁面。前文提到,Instant App只需一個鏈接就可以打開應用程序,通過鏈接方式Instant App和瀏覽器就完美兼容,對用戶來說無感知。我們選擇「打開應用」查看運行結果。

上圖頁面就是Instant App工程feature模塊主Activity。

執行adb命令。

adb shell dumpsys activity | grep "mFocusedActivity"

獲取當前手機正在顯示的Activity。

mFocusedActivity: ActivityRecord{fd0a677 u0 com.google.android.instantapps.supervisor/.shadow.ShadowActivity22 t2577}

從實際運行結果來看,正在運行Activity包名、類名並不是我們在feature模塊中聲明的Activity,實際類名為com.iqiyi.androidinstantapp.feature.MainActivity。熟悉插件化的朋友可能已經想到,該方案應該是通過「Activity預埋,運行時偷梁換柱」到達啟動插件中Activity目的。

為驗證Instant Apps是插件化框架猜想,我們找到google play services for instant apps的apk安裝包。通過反編譯工具jadx-gui查看其AndroidManifest.xml文件。

<service android:name="com.google.android.instantapps.supervisor.probe.probes.smoke.smoketests.hotpatching.IsolatedLinkerPatchingService" android:enabled="true" android:exported="false" android:process="com.google.android.instantapps.supervisor.probe.probes.smoke.smoketests.hotpatching.isolated" android:isolatedProcess="true"/>
...
<service android:name="com.google.android.instantapps.supervisor.shadow.ShadowService45" android:exported="false"/>
<service android:name="com.google.android.instantapps.supervisor.shadow.JobSchedulerShadowService1" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false"/>
...
<service android:name="com.google.android.instantapps.supervisor.shadow.JobSchedulerShadowService10" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false"/>
<activity android:theme="@style/Theme.ShadowActivity" android:name="com.google.android.instantapps.supervisor.shadow.ShadowActivity1" android:exported="false" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|layoutDirection|fontScale"/>
...
<activity android:theme="@style/Theme.ShadowActivity" android:name="com.google.android.instantapps.supervisor.shadow.ShadowActivity30" android:exported="false" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|layoutDirection|fontScale"/>
<activity android:theme="@style/Theme.TransparentShadowActivity" android:name="com.google.android.instantapps.supervisor.shadow.TransparentShadowActivity1" android:exported="false" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|layoutDirection|fontScale"/>
...
<activity android:theme="@style/Theme.TransparentShadowActivity" android:name="com.google.android.instantapps.supervisor.shadow.TransparentShadowActivity15" android:exported="false" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|layoutDirection|fontScale"/>

在Manifest文件中,聲明了大量預埋用的Activity和Service。另外通過簡要分析其代碼邏輯也驗證了我們之前的想法(有興趣的朋友可以深入分析gps for instant apps邏輯)。

從Android 8.0開始,Instant App核心邏輯均遷至Android Framework層,由系統層面提供支持,Android四大組件啟動無須通過「組件預埋,運行時偷梁換柱」方式。

在Android 8.0及以上設備執行adb命令(Android 8.0開始某些adb命令格式有所改變)。

adb shell dumpsys activity | grep "mResumedActivity"

獲取當前手機正在顯示的Activity。

mResumedActivity: ActivityRecord{dd97056 u0 com.iqiyi.androidinstantapp.app/com.iqiyi.androidinstantapp.feature.MainActivity t11}

從執行結果來看,Activity包名、類名與實際feature模塊中主Activity一致。通過閱讀Android 8.0 Framework源碼,可以看到不少Instant Apps相關的Api。

Android P 私有Api訪問限制

正當我們準備基於Instant App做愛奇藝組件化改造時,Android P對私有Api開始限制訪問。對於非SDK介面的限制中有介紹調用非SDK介面後果。

上圖中調用非SDK介面所引發的異常是指調用除淺灰名單以外所有私有Api。Android P對私有Api分為三個級別:淺灰名單、深灰名單、黑名單。調用深灰名單和黑名單私有Api在Android P設備上將會拋出上圖所列異常結果,調用淺灰名單私有Api不會拋出異常,但會輸出警告日誌。目前處於淺灰名單私有Api可能在後續Android版本中遷移至深灰或黑名單中。

從上述介紹可知,調用私有Api會出現一定風險。雖然已有黑科技可以繞過私有Api訪問檢查,但這些並不是長久之計。經過權衡,我們決定盡量避免調用私有Api。

Android P對私有Api訪問限制,並不是一刀切禁止所有私有Api,而是通過級別劃分,決定其危險級別。如果你實在需要調用某一深灰名單Api,你也可以提出申請,具體介紹參考Android 應用兼容性最佳實踐。即使目前處於淺灰名單Api,在後續Android版本中可能會提供SDK介面,Google還是很善於傾聽開發者意見。Android P私有Api訪問限制並不是洪水猛獸,它主要解決Android版本升級時,國內App兼容性很差的問題。

組件化蛻變

在最開始設計愛奇藝組件化時,就是希望盡量少調用私有Api,同時藉助Android提供打包插件完成打包工作。Android P私有Api訪問限制,更加讓我們堅定最初決策。Atlas最初是不支持新增四大組件,插件Manifest信息會合併至主客中,但即便如此還是存在較多私有api訪問,因為它是在組件啟動過程中去判斷插件是否安裝,以Activity為例。

調用Context#startActivity啟動Activity,會執行到Instrumentation#execStartActivity方法,Atlas做法是在該方法內嵌入插件是否安裝邏輯。此外還hook ActivityThread#mH同樣用於攔截Activity啟動判斷插件是否安裝。

Atlas的攔截系統啟動四大組件啟動過程判斷插件是否安裝,好處是對開發人員無任何侵入,都是基於Android SDK開發,但過多私有Api訪問給應用穩定性帶來挑戰,特別是Android P限制也帶來諸多不確定性。

Atlas雖然對開發人員無感知,但對後續Android版本升級適配存在較大風險,因此我們決定將Atlas對插件是否安裝判斷提供統一處理邏輯供開發人員調用。雖然這樣做會給開發人員帶來一定侵入性,但魚和熊掌不可兼得,為了組件化健康穩定發展犧牲一點便利性未嘗不可。當然,我們也可以通過AOP框架注入插件是否安裝邏輯,大致思路如下。

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent();
intent.setClassName(MainActivity.this, "com.iqiyi.demo.FeatureActivity");
startActivity(intent);
}
});
}

例如在啟動插件Activity時,是會調用startActivity,我們通過AOP框架掃描所有調用到該方法之處。加入以下邏輯。

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent();
intent.setClassName(MainActivity.this, "com.iqiyi.demo.FeatureActivity");
checkPlugin(intent);
}
});
}

public void checkPlugin(Intent intent) {
String className = intent.getComponent().getClassName();
String moduleName = PluginUtil.getModuleNameByClassName(className);
if (PluginUtil.checkPlugin(moduleName)) {
startActivity(intent);
} else {
PluginUtil.installPlugin(this, intent, smoduleNames);
}
}

Android App Bundle降臨

每次在我們組件化遇到瓶頸時,Google就會在關鍵時候給我們帶來驚喜。Instant Apps的打包插件雖然解決插件打包為apk,但我們還需處理以下問題。

  1. 將插件manifest信息合併至主客。
  2. aapt2打包出在系統5.0下會有異常(我們嘗試改過aapt2源碼解決了此問題)。

在我們開始解決以上問題時,Google推出Android App Bundle。關於AAB簡要介紹可以參考我們之前寫的一篇文章系統級插件化?Google全新的動態化框架Android App Bundles分析,感興趣朋友可以翻閱。AAB可以理解為一款全新的動態化框架,它是基於split apks完成,可有效減少應用程序包體積。

AAB與Instant Apps有何不同,我相信多數朋友會有此疑問。區別還是挺大的,Instant Apps是應用程序未下載,用戶通過鏈接即可體驗其部分功能,Instant Apps應用程序是運行在google play service上,而AAB插件是運行在咱們應用程序進程內。AAB強調的是減少app包體積同時提供一樣的用戶功能體驗,提供按需下載安裝模式。

上圖是總結AAB打包結構圖,從此圖可以發現它和我們最初設計組件化方案一致(AAB打包結構與Atlas類似,說明Atlas設計很具有前瞻性)。AAB打包結構中,業務插件、業務組件、主客一起打包輸出,業務插件的manifest信息會合併至主客中。需要說明的是,AAB並不支持新增Android四大組件.官方文檔有提到過未來AAB會與Instant Apps融合(google提出play instant),提供更加強大功能。

山寨Play Core Library

AAB提供Play Core Library供開發者下載安裝業務插件,感興趣朋友可體驗AAB官方示例。AAB看似一完美解決方案,但其需要google play service支持,國內環境無法使用,在國內必須提供下載安裝業務插件核心邏輯。所以經過考量,我們做出如下決定:

  1. 模仿Play Core Library提供的SDK,山寨出一套一模一樣SDK。好處是國際化版本走AAB,國內版本走自身組件化方案,無縫切換。
  2. 在AAB打包基礎上,增加定製化插件處理(非常輕量,易於維護)。

BundleInstallRequest request = BundleInstallRequest.newBuilder()
.addPackage("com.iqiyi.feature")
.addPackage("com.qiyi.cartoons")
.build();
installManager.startInstall(request).addOnSuccessListener(new OnSuccessListener<Integer>() {
@Override
public void onSuccess(Integer integer) {
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {

}
}).addOnCompleteListener(new OnCompleteListener<Integer>() {
@Override
public void onComplete(Task<Integer> task) {

}
});

以上代碼片段是我們山寨SDK第一期版本,Play Core Library SDK是經過混淆處理,因此花費了一定時間在山寨Play Core上。

新增Activity處理

前文提到,新增四大組件肯定會存在私有Api訪問,因此只能另尋他法。目前我們只支持「新增Activity」,Service、Receiver等暫不支持。Android提供更加細粒度視圖容器Fragment,用於視圖顯示,且Fragment無需在Manifest中聲明。因此我們將「新增Activity」降級為新增Fragment(改為新增Fragment對開發人員來說無任何侵入性),如此就能避免訪問過多私有Api。Fragment載入必須要以Activity為載體,因此我們需要預埋一些Activity,用於處理新增Fragment。在Google今天IO大會推出Android Jetpack navigation組件,用它可以非常方便處理Fragment跳轉,同時也提供一些類似Activity啟動模式特性。

需要注意的是,業務插件新增Activity,只是一種業務插件當前版本的臨時解決方案,我們應該在業務插件下個版本迭代中新增屬於它的真正Activity。

總結

在借鑒Google動態化方案做愛奇藝組件化過程中,也踩了相當多坑,限於本文篇幅,僅僅介紹愛奇藝組件化的演進過程以及設計初衷。如果有興趣深入交流的朋友,歡迎留言。Android動態化方案在未來的前景我們不敢妄下結論,但跟隨Google官方思路,會提供更佳的陽關大道。

彩蛋

  1. Instant App 資源Package Id大於0x7f。
  2. AAB 資源package Id小於0x7f。
  3. 第三方應用通過PackageInstaller安裝split apk是會調起系統安裝器。
  4. split apk中通過Resources#getIdentifier獲取資源id,packageName需要傳入應用程序包名加上split name。

推薦閱讀:

TAG:Android | Android開發 | 谷歌(Google) |