AAB 扶正!APK 再見!
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/hdpi
、lib/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 又根據language
、density
、abi
等三個維度,分別生成 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 的產物分為 splits
和 standalones
兩個目錄,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 中也無需配置 versionCode
、versionName
、signConfig
等,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
- Android Studio Electric Eel 起支援手機投屏
- Compose 為什麼可以跨平臺?
- 一看就懂!圖解 Kotlin SharedFlow 快取系統
- 深入淺出 Compose Compiler(2) 編譯器前端檢查
- 深入淺出 Compose Compiler(1) Kotlin Compiler & KCP
- Jetpack MVVM七宗罪之三:在 onViewCreated 中載入資料
- 為什麼說 Compose 的宣告式程式碼最簡潔 ?Compose/React/Flutter/SwiftUI 語法對比
- Compose 型別穩定性註解:@Stable & @Immutable
- Fragment 這些 API 已廢棄,你還在使用嗎?
- 告別KAPT!使用 KSP 為 Kotlin 編譯提速
- 探索 Jetpack Compose 核心:深入 SlotTable 系統
- 盤點 Material Design 3 帶來的新變化
- Compose 動畫邊學邊做 - 夏日彩虹
- Google I/O :Android Jetpack 最新變化(二) Performance
- Google I/O :Android Jetpack 最新變化(一) Architecture
- Google I/O :Android Jetpack 最新變化(四)Compose
- Google I/O :Android Jetpack 最新變化(三)UI
- 一文看懂 Jetpack Compose 快照系統
- 聊聊 Kotlin 代理的“缺陷”與應對
- AAB 扶正!APK 再見!