AAB 扶正!APK 再見!

語言: CN / TW / HK

theme: v-green highlight: androidstudio


本文已參與好文召集令活動,點選檢視:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!

Google 自8月起要求 Google Play 上架的應用必須採用 AAB 的新格式,對我來說這並非新聞,早在去年12月份官方就提前做了通知:

http://android-developers.googleblog.com/2020/11/new-android-app-bundle-and-target-api.html

令我驚訝的是,這樣一條“舊聞”最近卻被炒得沸沸揚揚,原來竟還是因為蹭了鴻蒙的熱度:

要知道 AAB 的首次亮相是在2018年的 GoogleI/O 上,難道彼時谷歌就遇見到鴻蒙的出現了?

不過客觀來說,AAB 雖然早已出現,但在國內很少被提及,因此造成部分媒體的錯誤解讀也有情可原。那麼本文就為大家做一個關於 AAB 的科普,打消鴻蒙支持者們的顧慮。

Android App Bundle

Android App Bundle(簡稱AAB) 是 Google 2018年推出的一種動態化的打包方式。當應用程式以 AAB 的格式上傳 Google Play(或其他支援 AAB 的應用市場)後,可以根據不同使用者實現 features 或者 resources 的按需下發。Google Play (簡稱GP) 目前提供的動態化服務都是基於 AAB 實現的(不少文章說這些服務是 AAB 的,這種說法不嚴謹,準確的說是 GP 的)

  • Play Feature Delivery(PFD) :藉助 AAB 實現 Feature 的按需動態載入,這類似於國內流行的“外掛化”技術
  • Play Asset Delivery (PAD) :藉助 AAB 實現一些資源素材的按需動態下載,這特別適合一些遊戲類APP,無需為了適配所有機型保留全部遊戲素材

除了遊戲資源以外,對於常規資源,AAB 也可以做到按需下發。例如無需同時存在 hdpi、xhdpi 等多套圖片,不少 APP 因此在包大小方面有顯著提高:

更小的包體積意味著更高的裝機率,這在使用者推廣成本激高的今天至關重要:

App Bundle 檔案格式

我們先來看一下 AAB 的檔案格式,與傳統的 APK 有何不同

解壓後的 AAB 中的內容和 APK 很相似,但又有不少區別:

|aab fiels| descriptions| |:--|:--| |base/feature1/feature2| base 是應用的基本功能,feature 承載各 DynamicFeature 的內容(後文介紹)| |manifest.xml| APK 中只有一個 manifest 且是二進位制格式,AAB 會存在於每個模組中e中,且使用 ProtoBuf(pb)格式,便於處理| |dex|與 APK 不同,AAB 將每個模組的 dex 檔案儲存在各自目錄中| |res/assets/libs|該目錄與 APK 中相同,當上傳 AAB 時,GP 會檢查這些目錄並僅打包滿足目標裝置需要的最小檔案| |resources.pb| 類似於 resource.arsc 檔案,是一個資源索引表,其中描述了應用程式內部存在的資源和目標的細節,可用於 GP 針對不同裝置配置 APK。| |assets.pb|相當於應用程式 assets 的資源表,可用於 GP 針對不同裝置配置 APK。例如將 assets 資源放到 assets/languages#lang_xx 或 assets/i18n#lang_xx 路徑下,則會根據語言配置下發 assets 資源。| |native.pb| 這相當於native庫的資源表,可用於 GP 針對不同裝置配置 APK|

後三個.bp檔案是 AAB 格式的重要部分,它們描述了 APP 的不同服務目標,動態下發根據這些目標從 drawable/hdpilib/armeabi-v7a 或者 values/es 等路徑中組織不同資源進行下發。

Split APKs

Split APKs 機制是 AAB 實現動態下發的基礎,它允許將一個龐大的 APK 按不同維度拆分成獨立的 APK,當用戶在 GP 下載應用時,Android Framework 通過 IPC 與 GP 通訊,為當前裝置匹配並下載最小構成的 APK, 這隻在 Android 5.0 以上的裝置才有效。

AAB 上傳後,GP 通過分析找出所有裝置的共同資源, 生成一個 Base APK,當用戶下載應用時,Base APK 將被首先安裝。

GP 又根據languagedensityabi 等三個維度,分別生成 Configuration APKs(Splits), Splits 與 Base 共享 versionCode 、packageName等,在程序管理器中以一個應用的形式存在。

當用戶從市場下載應用時,GP 根據裝置型別,為其下發不同的 Splits,實現最小化下發。

如下圖,針對三種不同裝置下發不同 Splits

當用戶的裝置發生 Configuration Changed (比如切換了系統語言)時,GP 會下發新的 Splits 到手機,如果此時手機不線上會等待下次上線時自動下發。

Split APKs 的這種動態下發只能用於 Android 5.0 以上裝置,對於更舊的裝置,AAB 會根據 這些 Splits 的矩陣生成多個 Standalone 的 APK,雖然缺少了動態下發的能力必須一次安裝到位,但是相對於傳統 APK 仍然減小了一定包大小。

作為開發者,我們無需關心這些具體的下發策略,只需要向市場上傳一個 AAB ,後續就交給 FW 和 GP 去處理了。

建立 App Bundle

打包 AAB

使用 Android Studio 可以方便地打包 AAB

此外,也可以使用 Gradle 命令打包,這更適用於一些 CI 流程中。

如下使用 gradle 打包一個 debug 版的 AAB

groovy ./gradlew :base:bundleDebug 如果要生成 release 的 AAB 需要配置簽名,與 APK 的配置方式是一樣的。

AAB 預設會為三種 Configurations 都生成 Splits,當然你可以根據需求自己配置: groovy bundle { language { enableSplit = false } density { enableSplit = true } abi { enableSplit = true } }

上傳應用市場

生成 AAB 後就可以上傳應用市場了,GP 中上傳 AAB 和 APK 的入口在一起,當然 8 月以後就沒有 APK 的上傳入口了。

AAB 上傳後,通過後臺可以檢視其詳細資訊

例如可以檢視 AAB 支援的螢幕密度,以及包體積的減少等資訊

Bundle Tool

AAB 是無法直接安裝到手機的,如果想本地對 AAB 做測試,需要將 AAB 轉成 APK,這需要使用 Google 官方提供的 Bundletool 工具

Bundletool 可以獲取當前裝置資訊 bundletool get-device-spec --output=/tmp/device-spec.json 裝置的 Configurations 資訊輸出到指定 json 中 json { "supportedAbis": ["arm64-v8a", "armeabi-v7a", "armeabi"], "supportedLocales": ["zh-CN"], "deviceFeatures" : // ... "screenDensity": 480, "sdkVersion": 28 }

Bundletool 根據 json 生成 .apks 中間檔案 ``` bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks --ks=/MyApp/keystore.jks --ks-pass=file:/MyApp/keystore.pwd --ks-key-alias=MyKeyAlias --key-pass=file:/MyApp/key.pwd --device-spec=file:device-spec.json

```

apks 的產物分為 splitsstandalones 兩個目錄,splits 是按照 Configuration維度拆分的 Split APKs,必須依賴 base.apk 一起安裝;standalone 必須獨立安裝,這是為了相容 Android 5.0 以下的版本。

toc.pb 是 apks 的存檔清單,包含 APK 集合資訊的描述檔案

然後再根據 json 檔案,從 apks 中提取 apk : ``` bundletool extract-apks --apks=${apksPath} --device-spec={deviceSpecJsonPath} --output-dir={outputDirPath}

```

最後,通過 Bundletool 將 apk 安裝到手機上。 注意該命令實際安裝 apk 並非 apks ``` bundletool install-apks --apks=/MyApp/my_app.apks

```

總結一下 Bundletool 生成 APK 的整體流程:

建立 Dynamic Feature

除了下發 Configuration APKs,還可以以業務模組為單元“外掛化”地動態下發,也就是所謂的 Dynamic Features(簡稱 DF)

IDE 中選擇 New 一個 DF 的 Module:

點選 next,選擇 DF 的安裝時機,例如一次安裝到位或是按需安裝

建立好的 DynamicFeature Module, 目錄和一個普通的 Gadle Module 類似

但是 build.gradle 中 plugin 有所不同:com.android.dynamic-feature groovy plugins { id 'com.android.dynamic-feature' id 'kotlin-android' } build.gradle 中也無需配置 versionCodeversionNamesignConfig等,DF 本質上也是 Split APKs,所以共享 Base APK 的這些資訊。

此時再開啟 app/ 的 build.gradle,會發現多瞭如下配置

groovy dynamicFeatures = [':dynamicfeature'] 這是 APP 當前支援的所有 DF 的宣告

最後,DF 的 Manifeset 也發生了變化:

```xml

<dist:module
    dist:instant="false"
    dist:title="@string/title_dynamicfeature">
    <dist:delivery>
        <dist:on-demand />
    </dist:delivery>
    <dist:fusing dist:include="true" />
</dist:module>

``` - dist:delivery: 在建立 Dynmaic Feature 的 Module 時選擇的下發方式, onDemand 表示方式為按需下發 - title:當用戶確認下載 Module 時,標識相關名稱 - fusing include:設為 ture,意味著 5.0 以下的裝置可以以 multi-APK 的形式安裝此 Feature,此時必須設定為 onDemand 方式。

安裝 Dynamic Feature

當應用支援 DF 之後,我可以按需的請求並安裝這些 Features,這需要整合 Play Core SDK groovy implementation 'com.google.android.play:core:$latest_version'

Play Core 允許使用者通過互動的方式請求 DF 的下載安裝,並監聽下載狀態

發起下載請求

DF 的下載需要藉助 SplitInstallManager

kotlin SplitInstallManager splitInstallManager = SplitInstallManagerFactory.create(context); 建立 SplitInstallRequest, 請求下載 Module kotlin //動態請求模組 SplitInstallRequest request = SplitInstallRequest .newBuilder() .addModule("someDynamicModule") .build(); addModule() 可以多次呼叫,新增多個請求的 DF

使用 SplitInstallManager 啟動 Request 進行請求,並設定回撥監聽為下載狀態

kotlin splitInstallManager .startInstall(request) .addOnSuccessListener { } .addOnFailureListener { } .addOnCompleteListener { } startInstall() 呼叫後會立即發起請求。另外還可以使用 deferredInstall 延遲請求, 當應用切到後臺啟動時才開始請求。

kotlin splitInstallManager .deferredInstall(Arrays.asList("someDynamicModule"));

除了請求指定 DF 以外,也可以請求指定的資源,比如安裝語言資源

kotlin SplitInstallRequest request = SplitInstallRequest.newBuilder() .addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION))) .build();

發起請求後,會返回一個 Int 值作為 session ID,通過呼叫 cancelInstall(Int), 可以取消當前的下載。

發起請求後,可能無法正常建立連結,此時會返回錯誤資訊如下 |Error| Descriptions| |:--|:--| |ACCESS_DENIED | 鑑於當前裝置的某些原因,無法下載| |ACTIVE_SESSIONS_LIMIT_EXCEEDED |當前應用的請求 session 太多| |API_NOT_AVAILABLE |請求 API 目前無法使用| |INCOMPATIBLE_WITH_EXISTING_SESSION |請求的 session 中包含了已經請求中的 DF| |INTERNAL_ERROR |內部錯誤| |INVALID_REQUEST |無效請求| |MODULE_UNAVAILABLE |請求的 DF 不存在| |NETWORK_ERROR | 網路錯誤| |NO_ERROR | 無法獲得錯誤資訊 | |SERVICE_DIED |服務無響應| |SESSION_NOT_FOUND |無法獲取被請求的 session|

下載安裝

成功建立了連線後,便進入下載、安裝階段。使用 SplitInstallStateUpdatedListener 能夠監聽下載安裝的狀態,可以根據這些狀態為對下載進度等進行使用者提示

kotlin val stateListener = SplitInstallStateUpdatedListener { state -> when (state.status()) { PENDING -> { } DOWNLOADING -> { } DOWNLOADED -> { } INSTALLED -> { } INSTALLING -> { } REQUIRES_USER_CONFIRMATION -> { } FAILED -> { } CANCELING -> { } CANCELED -> { } } } splitInstallManager.registerListener(stateListener) |State| Description| |:--|:--| |CANCELED| 下載被取消| |CANCELING| 下載取消中| |DOWNLOADED| 下載完成,但是尚未安裝| |DOWNLOADING| 下載即將完成| |FAILED| 下載或安裝失敗| |INSTALLED| 成功安裝| |INSTALLING| 安裝中| |PENDING|下載等待中| |REQUIRES_USER_CONFIRMATION| 等待使用者確認| |UNKNOWN|未知|

解除安裝模組

成功安裝後,通過 getInstalledModules 可以獲取所有已安裝的 Module kotlin val installedModules = splitInstallManager.installedModules 另外,通過 deferredUninstall 可以對 DF 進行指定解除安裝 ```kotlin splitInstallManager .deferredUninstall(listOf("someDynamicModule")) .addOnSuccessListener { } .addOnFailureListener { } .addOnCompleteListener { }

```

AAB 使用效果

根據 Google 官方的資料,AAB 比 APK 的包大小平均會減小 20% ,這同時意味著節省了 20% 的下載流量。 以 Twitter 為例,採用 AAB 之後

  • language 相關資源節省 95%
  • density 相關的 Splits 節省 45%
  • abi 相關資源節省 20%

除了包大小方面的優勢以外,使用 AAB 在開發效率上也有收益,無需再針對不同目標點裝置,配置多個 Flavor、生成多個 APK 並分別上傳,只要上傳一個 AAB,剩下的事情交由應用市場去做就好了。

國內的 AAB 使用

Qigsaw 是愛奇藝提供的一套基於 Android App Bundle 的動態化方案,無需 Google Play Service 即可在國內體驗 Android App Bundle 開發工具。它支援動態下發外掛 APK,採用單類載入器方式,讓應用能夠在不重新安裝的情況下實現動態安裝外掛。

此外,華為應用市場也早就支援了 AAB 的上傳和動態下發,所以不要再說 AAB 是打壓華為的產物了 😅

http://developer.huawei.com/consumer/cn/doc/distribution/app/agc-help-releasebundle-0000001100316672