[TOC]
# AAB
## App Bundle 文件格式
Android App Bundle是一種全新的應用上傳格式(.aab),它包含所有編譯代碼和資源。當您上傳aab文件至Google Play后,Google Play將aab文件拆分成一系列APKs并簽名。


### 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` 等路徑中組織不同資源進行下發。
## Dynamic Feature
此外,您也可以在應用項目中添加dynamic feature模塊,這些模塊并不需要在應用首次安裝時一起被下載安裝。您可以通過使用Play Core Libray在應用運行過程中動態安裝dynamic feature。dynamic feature類似國內插件化提供的能力,但dynamic feature功能更強大。
通過下圖,可以看到dynamic feature可以基于設備配置選取對應的Configuration Split APKs,如此可以進一步減小dynamic feature安裝包體積。

### Split APKs(Android5.0)
Android App Bundle之所以能夠支持應用運行期間安裝dynamic feature,得益于Android 5.0推出的Split APKs功能。
Split APKs是Android 5.0引入的一種全新應用安裝機制,其目的是為解決APK體積日益增大問題。Split APK可以將一個完整龐大的APK按照CPU架構、屏幕密度等維度拆分成多個獨立APKs。當應用APK下載更新時,依據當前設備配置選取對應配置APKs安裝即可。
Android 5.0之前,一個APK代表一個應用。在Split APKs問世之后,一個應用可能對應多個APKs。所有Split APKs擁有相同包名和簽名。
Android提供兩種方式安裝Split APKs。
adb install-multiple [base-apk, split1-apk]
PackageInstaller.
Android App Bundle為dynamic feature提供全新插件com.android.dynamic-feature,它的編譯產物是.apk文件。當您的項目編譯完成后,Android Studio通過命令adb install-multiple命令將base apk和split apks安裝至您的手機。如果您的開發手機系統版本低于5.0,則會依據當前手機設備組裝成一個完整apk文件安裝至該手機。

下面我們重點介紹第二種安裝方式,Android 5.0提供PackageInstaller用于安裝Base APK和Split APKs。
#### PackageInstaller
當第三方應用通過PackageInstaller在應用運行期安裝Split APKs時,系統會啟動安裝器界面供用戶選擇是否安裝此次更新。

在用戶選擇安裝后,應用將會被系統“殺死”。當應用再次啟動之后,Split APKs就會生效。
在我們實際測試過程中,某些國產手機對PackageInstaller有改動,導致無法正常安裝Split APKs。
系統應用可以靜默安裝Split APKs,且當Split APKs安裝完成后,可以決定是否“殺死“應用進程。
```
public static class SessionParams implements Parcelable {
/** {@hide} */
@SystemApi
public void setDontKillApp(boolean dontKillApp) {
if (dontKillApp) {
installFlags |= PackageManager.INSTALL_DONT_KILL_APP;
} else {
installFlags &= ~PackageManager.INSTALL_DONT_KILL_APP;
}
}
...
...
}
```
SessionParams是PackageInstaller內部類,setDontKillApp可決定當APK安裝完成后是否殺死應用進程。setDontKillApp屬于系統Api,因此第三方應用無法調用。
### Split APKs加載原理

通過Android 9.0 LoadedAPK源碼片段,我們一起了解下Split APKs加載過程。
是的,系統級的代碼本身就支持了Split Apk的加載
#### ClassLoader
通過createOrUpdateClassLoaderLocked方法名,可以知道該方法是用于創建和更新ClassLoader

該方法有兩個核心步驟。
如果mClassLoader為空,則創建PathClassLoader實例。
如果addedPaths不為空,則更新PathClassLoader實例
該方法指明,應用進程是可以動態加載Split APKs代碼
#### Resources
通過getResources方法代碼片段,可知Split APKs的資源路徑作為mResources創建參數

#### 不支持四大組件的新增
Android App Bundle在Manifest文件合并過程中,會將split APKs manifest文件內容合并至base APK中。因此,所有split APKs四大組件信息都是已經聲明在base APK中。
Android App Bundle這種處理方式不支持Manifest更新,例如新增四大組件
#### 多進程問題
Android App Bundle所支持的功能特性有部分局限性
多進程問題
依據Qigsaw安裝、加載split APKs原則,當游戲APK安裝完成后,就會在主進程完成加載。在游戲APK中有兩個Activity,他們所處進程不同。當啟動GameActivity01時,頁面正常啟動。但當啟動GameActivity02,您的App會出現崩潰。原因是GameActivity02運行在:game進程,游戲APK僅在主進程加載,并未在:game進程加載,因此系統會拋出ClassNotFoundException異常。
為解決這類問題,Qigsaw提供了如下解決方案。
在進程啟動之初即Applicatin#attachBaseContext調用時,加載所有已安裝splits。
第一種方案解決的場景是:game進程首次啟動,即啟動GameActivity02之前:game進程從未啟動過。
Hook PathClassLoader。
第二種方案解決的場景是:game進程已經啟動并正在運行
Hook PathClassLoader具體做了如下事情。
當出現ClassNotFoundException時,判斷該類是否為splits四大組件。
當異常類為splits四大組件時,加載所有已安裝未加載split APKs。
如加載完所有已安裝未加載split APKs后依然出現ClassNotFoundException異常,則返回空四大組件類,防止進程崩潰。
# 愛奇藝動態化框架Qigsaw
## 背景知識
在2018年上半年,我們就進行動態組件化方案的調研。起初方案是基于Instant App方案實現,當整體功能基本實現后,Google于2018年Google IO大會上推出Android App Bundle。在調研Android App Bundle之后,我們發現Android App Bundle完全符合最初的需求。
依據我們最初設計初衷和Android App Bundle特點,總結出Qigsaw應滿足以下核心特點。
利用Android App Bundle開發套件,體驗原生極速開發體驗。
少量私有Api訪問,保證框架穩定性。
如果您的應用有出海需求,可無縫切換至Android App Bundle方案。
關于私有Api訪問應該是大家比較關心的,最近一段時間某大廠開源了號稱零反射插件化框架,但是通過閱讀其源碼,我們發現它還是做了PathClassLoader的parent ClassLoader反射替換。
另外它也調用了Resources構造方法創建Resources實例,雖然這樣做并沒有任何私有Api訪問,但是通過查看Resources構造方法源碼,我們可知該方法屬于過時方法,且注釋寫明第三方應用不應該創建Resources實例。
所以插件化框架不應該僅僅以是否零反射為目標,我們應該從開發流程及產品形態選取合適方案,助力開發效率。
## 比googlePlay更好的打包體驗
在發布階段,Qigsaw提供打包插件讓開發者享受一條龍服務,開發者不必關心dynamic feature的上傳分發。
Qigsaw打包插件支持內置dynamic feature,所有內置dynamic feature都會被拷貝至base apk的assets目錄。對于非內置dynamic feature,Qigsaw打包插件會將其上傳至CDN服務器,解決業務方后顧之憂。
## Qigsaw原理還是基于插件化
Qigsaw借助Android App Bundle開發套件完成dynamic feature的打包,大大降低Qigsaw開發維護成本。因此Qigsaw關心的重點落在 如果安裝加載dynamic feature生成apk上。
第三方應用利用PackageInstaller安裝split APKs體驗極其不友好,且某些國產手機對split APKs功能支持不完善,所以我們最終還是按照一般插件化方式安裝加載split APKs。

依據上圖,如果需要動態加載split APKs,需要解決代碼、資源以及四大組件的加載
### 代碼加載使用打補丁
針對splits代碼加載,Qigsaw采用單類加載器方式,即base APK和split APKs采用同一ClassLoader加載。

在DexPathList中,為每個split創建對應的Element和NativeLibraryElement實例即可。關于單類加載器更多細節,本文不再贅述,相關原理已非常成熟。
### 資源加載基于打補丁的方式同時不會沖突

Splits資源加載相較于代碼加載會復雜,因為不同系統版本或不同手機廠商都會存在一些兼容性問題。
資源id沖突問題
Android Gradle Plugin在資源打包時,會對res目錄下資源文件分配一個唯一Id。
* Id前兩位PP為Package Id,代表應用類型。是系統應用、第三方應用、Instant App或Dynamic Feature等。
* Id中間兩位TT為Type,代表資源類型。是drawable、layout或string等。
* Id后四位EE為Entry,代表該資源順序。
所有第三方應用base APK資源Package Id均為7F,Android App Bundle對splits資源打包時會基于7F依次遞減分配Package Id。因此,即使我們將split APKs資源添加到當前應用Resources實例中,也不會出現資源沖突問題,splits訪問base資源也更加方便<.font>
Instant Apps資源打包是基于7F依次遞增。
**反射Resource對象**

Qigsaw提供loadResources方法加載split APKs資源。為避免開發者寫大量模板代碼,Qigsaw打包插件采用字節碼操作方式自動寫入該方法。
### 不支持四大組件的新增
Android App Bundle在Manifest文件合并過程中,會將split APKs manifest文件內容合并至base APK中。因此,所有split APKs四大組件信息都是已經聲明在base APK中。
Android App Bundle這種處理方式不支持Manifest更新,例如新增四大組件,所以Qigsaw也不支持新增四大組件。在正常開發迭代過程中,動態新增splits四大組件需求極少,所以Qigsaw與Android App Bundle特性保持一致。
# 參考資料
[AAB 扶正!APK 再見!](https://juejin.cn/post/6984588418554527774)
[# Android App Bundle動態化方案](https://blog.csdn.net/fei20121106/article/details/108034651)
- Android
- 四大組件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介紹
- MessageQueue詳細
- 啟動流程
- 系統啟動流程
- 應用啟動流程
- Activity啟動流程
- View
- view繪制
- view事件傳遞
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大數據
- Binder小結
- Android組件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 遷移與修復
- Sqlite內核
- Sqlite優化v2
- sqlite索引
- sqlite之wal
- sqlite之鎖機制
- 網絡
- 基礎
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP進化圖
- HTTP小結
- 實踐
- 網絡優化
- Json
- ProtoBuffer
- 斷點續傳
- 性能
- 卡頓
- 卡頓監控
- ANR
- ANR監控
- 內存
- 內存問題與優化
- 圖片內存優化
- 線下內存監控
- 線上內存監控
- 啟動優化
- 死鎖監控
- 崩潰監控
- 包體積優化
- UI渲染優化
- UI常規優化
- I/O監控
- 電量監控
- 第三方框架
- 網絡框架
- Volley
- Okhttp
- 網絡框架n問
- OkHttp原理N問
- 設計模式
- EventBus
- Rxjava
- 圖片
- ImageWoker
- Gilde的優化
- APT
- 依賴注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 協程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 運行期Java-hook技術
- 編譯期hook
- ASM
- Transform增量編譯
- 運行期Native-hook技術
- 熱修復
- 插件化
- AAB
- Shadow
- 虛擬機
- 其他
- UI自動化
- JavaParser
- Android Line
- 編譯
- 疑難雜癥
- Android11滑動異常
- 方案
- 工業化
- 模塊化
- 隱私合規
- 動態化
- 項目管理
- 業務啟動優化
- 業務架構設計
- 性能優化case
- 性能優化-排查思路
- 性能優化-現有方案
- 登錄
- 搜索
- C++
- NDK入門
- 跨平臺
- H5
- Flutter
- Flutter 性能優化
- 數據跨平臺