這是【Android 修煉手冊】系列第 10 篇文章,若是尚未看過前面系列文章,歡迎點擊 這裏 查看~html
咱們如今大部分開發都是基於 Android Studio 進行的,在 AS 中,咱們只須要點擊 Run 按鈕,AS 自動會打包 Apk 安裝至設備中並運行。對於咱們來講,其中的打包過程就是一個黑盒,咱們所知道的就是 Sources -> AS Compiler -> APK。這篇文章咱們就分析一下中間的打包過程,以及打包相關的一些問題。java
咱們先了解一下 APK 內部的結構。android
接下來咱們看看一個正常的 APK 的結構。
一個 APK 打包完以後,一般有下面幾個目錄,用來存放不一樣的文件。
assets
原生資源文件,不會被壓縮或者處理
classes.dex
java 代碼經過 javac 轉化成 class 文件,再經過 dx 文件轉化成 dex 文件。若是有多個 dex 文件,其命名會是這樣的:
classes.dex classes2.dex classes3.dex ...
在其中保存了類信息。
lib/
保存了 native 庫 .so 文件,其中會根據 cpu 型號劃分不一樣的目錄,好比 ARM,x86 等等。
res/
保存了處理後的二進制資源文件。
resources.arsc
保存了資源 id 名稱以及資源對應的值/路徑的映射。
META-INF/
用來驗證 APK 簽名,其中有三個重要的文件 MANIFEST.MT,CERT.SF,CERT.RSA。
MANIFEST.MF 保存了全部文件對應的摘要,部份內容以下:git
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 3.4.0
Name: AndroidManifest.xml
SHA-256-Digest: QxJh66y6ssDSNFgZSlf5jIWXfRdWnqL1c3BSwSDUYLQ=
Name: META-INF/android.arch.core_runtime.version
SHA-256-Digest: zFL2eISLgUNzdXtGA4O/YZYOSUPCA3Na3eCjULPlCYk=
複製代碼
CERT.SF 保存了MANIFEST.MF 中每條信息的摘要,部份內容以下:github
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA-256-Digest-Manifest: j8YGFgHsujCHud09pT6Igh21XQKSnG+Gqy8VUE55u+g=
X-Android-APK-Signed: 2
Name: AndroidManifest.xml
SHA-256-Digest: qLofC3g32qJ5LmbjO/qeccx2Ie/PPpWSEPBIUPrlKlY=
Name: META-INF/android.arch.core_runtime.version
SHA-256-Digest: I65bgli5vdqHKel7MD74YlSuuyCR/5NDrXr2kf5FigA=
複製代碼
CERT.RSA 包含了對 CERT.SF 文件的簽名以及簽名用到的證書。web
AndroidManifest.xml
這個文件你們都很熟悉了,全局配置文件,不過這裏是編譯處理過的二進制的文件。shell
咱們在上面看了一個完整 APK 的結構,APK 打包的過程其實就是生成上述文件的過程。這裏放一張網上流傳比較廣的流程圖。
api
主要有下面幾個步驟:bash
平時的開發都是使用 gradle 構建,下面咱們不依賴 gradle,直接用官方提供的各個階段的打包工具,手動用命令行打一個 APK,能夠更好更詳細的瞭解其中的過程。 這裏直接用 simpleapk 工程作示例。
官方的打包工具在 android_sdk/build-tools/version/ 目錄下。架構
開始以前,咱們先建立一個 tmp 目錄用來存放中間產物。建立 tmp/final 存放最終產物。
使用 AAPT2 處理資源須要兩步,compile 和 link,首先執行 compile 操做。執行下面的命令。
/Users/zy/android-sdk-mac_x86/build-tools/28.0.2/aapt2 compile -o tmp/res --dir src/main/res/
複製代碼
使用 aapt2 對單個資源處理,會生成 xxx.flat 文件,是 aapt2 的中間產物,能夠用於後面的資源增量編譯。咱們這裏經過 --dir 直接指定了資源的目錄,產物 res 是一個壓縮包,裏面包含了全部資源處理後的 xxx.flat。
這裏咱們再把 res 這個壓縮包解壓一下。執行下面的命令。
unzip -u tmp/res -d tmp/aapt2_res
複製代碼
這一步結束之後,目錄是這個樣子的。
tmp/
├── aapt2_res
│ └── xxx.flat
├── final
└── res
複製代碼
AAPT2 link 是把上一步 compile 處理後的 xxx.flat 資源連接,生成一個完整的 resource.arsc,二進制資源和 R.java。執行下面的命令。
/Users/zy/android-sdk-mac_x86/build-tools/28.0.2/aapt2 link -o tmp/res.apk -I /Users/zy/android-sdk-mac_x86/platforms/android-28/android.jar --manifest src/main/AndroidManifest.xml --java tmp -R tmp/aapt2_res/drawable-hdpi_ic_launcher_foreground.xml.flat -R tmp/aapt2_res/mipmap-anydpi-v26_ic_launcher_round.xml.flat -R tmp/aapt2_res/mipmap-xhdpi_ic_launcher_round.png.flat -R tmp/aapt2_res/values_colors.arsc.flat -R tmp/aapt2_res/drawable-hdpi_ic_launcher_foreground1.xml.flat -R tmp/aapt2_res/mipmap-hdpi_ic_launcher.png.flat -R tmp/aapt2_res/mipmap-xxhdpi_ic_launcher.png.flat -R tmp/aapt2_res/values_strings.arsc.flat -R tmp/aapt2_res/drawable-mdpi_ic_launcher_foreground.xml.flat -R tmp/aapt2_res/mipmap-hdpi_ic_launcher_round.png.flat -R tmp/aapt2_res/mipmap-xxhdpi_ic_launcher_round.png.flat -R tmp/aapt2_res/values_styles.arsc.flat -R tmp/aapt2_res/drawable_ic_launcher_background.xml.flat -R tmp/aapt2_res/mipmap-mdpi_ic_launcher.png.flat -R tmp/aapt2_res/mipmap-xxxhdpi_ic_launcher.png.flat -R tmp/aapt2_res/layout_activity_main.xml.flat -R tmp/aapt2_res/mipmap-mdpi_ic_launcher_round.png.flat -R tmp/aapt2_res/mipmap-xxxhdpi_ic_launcher_round.png.flat -R tmp/aapt2_res/mipmap-anydpi-v26_ic_launcher.xml.flat -R tmp/aapt2_res/mipmap-xhdpi_ic_launcher.png.flat --auto-add-overlay
複製代碼
執行命令後,會生成 res.apk,裏面就是 resource.arsc,處理後的 AndroidManifest.xml 以及 處理後的二進制資源。咱們這裏也把他解壓出來,後面最終打包的時候使用。執行命令以下。
unzip -u tmp/res.apk -d tmp/final
複製代碼
這一步結束之後,目錄狀態以下。
tmp/
├── aapt2_res
│ └── xxx.flat
├── com
│ └── zy
│ └── simpleapk
│ └── R.java
├── final
│ ├── AndroidManifest.xml
│ ├── res
│ │ ├── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── xxx 資源目錄
│ └── resources.arsc
├── res
└── res.apk
複製代碼
這一步咱們須要處理 java 文件,生成 class 文件。要用到上一步生成的 R.java 文件。執行下面的命令。
javac -d tmp src/main/java/com/zy/simpleapk/MainActivity.java tmp/com/zy/simpleapk/R.java -cp /Users/zy/android-sdk-mac_x86/platforms/android-28/android.jar
複製代碼
這一步結束之後,目錄狀態以下。
tmp/
├── aapt2_res
│ └── xxx.flat
├── com
│ └── zy
│ └── simpleapk
│ ├── MainActivity.class
│ ├── R$color.class
│ ├── R$drawable.class
│ ├── R$layout.class
│ ├── R$mipmap.class
│ ├── R$string.class
│ ├── R$style.class
│ ├── R.class
│ └── R.java
├── final
│ ├── AndroidManifest.xml
│ ├── res
│ │ ├── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── xxx 資源目錄
│ └── resources.arsc
├── res
└── res.apk
複製代碼
這一步是把上一步生成的 class 文件編譯爲 dex 文件,須要用到 d8 或者 dx,這裏用 d8。執行下面的命令。
/Users/zy/android-sdk-mac_x86/build-tools/28.0.2/d8 tmp/com/zy/simpleapk/*.class --output tmp --lib /Users/zy/android-sdk-mac_x86/platforms/android-28/android.jar
複製代碼
命令執行完之後,會生成 classes.dex,這就是最終 apk 裏須要的 dex 文件,咱們把它拷貝到 final/ 目錄下。執行以下命令。
cp tmp/classes.dex tmp/final/classes.dex
複製代碼
這一步結束之後,目錄狀態以下。
tmp/
├── aapt2_res
│ └── xxx.flat
├── classes.dex
├── com
│ └── zy
│ └── simpleapk
│ ├── MainActivity.class
│ ├── R$color.class
│ ├── R$drawable.class
│ ├── R$layout.class
│ ├── R$mipmap.class
│ ├── R$string.class
│ ├── R$style.class
│ ├── R.class
│ └── R.java
├── final
│ ├── AndroidManifest.xml
│ ├── classes.dex
│ ├── res
│ │ ├── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── xxx 資源目錄
│ └── resources.arsc
├── res
└── res.apk
複製代碼
執行完上述的命令,打包 APK 須要的材料就都準備好了,由於 APK 自己就是 zip 格式,這裏咱們直接用 zip 命令打包上述產物,生成 final.apk。執行下面的命令。
zip -r final.apk *
複製代碼
上一步打包好的 APK 還不能直接安裝,由於沒有簽名,咱們這裏用 debug.keystore 給 final.apk 簽名。執行下面的命令。
/Users/zy/android-sdk-mac_x86/build-tools/28.0.2/apksigner sign --ks ~/.android/debug.keystore final.apk
複製代碼
這裏須要輸入 debug.keystore 的密碼,是 android。
這樣,最後的 final.apk 就是咱們手動生成的 apk 了。能夠安裝嘗試一下了~
正常開發中,咱們大部分都是直接點擊 AS 中的 Run 去編譯一個 APK。
咱們先來看看在 AS 中點擊 Run 作了些什麼事情。咱們點擊 Run 運行,而後打開 Build tab 看下具體的輸出,能夠發現執行的是 Gradle Task assembleDebug。
咱們在 build.gradle 裏添加一行輸出,看看 AS 給 gradle task 添加的額外參數。
println "projectProperties: " + project.gradle.startParameter.projectProperties
複製代碼
輸出以下:
projectProperties: [android.injected.build.density:xhdpi, android.injected.build.api:26, android.injected.invoked.from.ide:true, android.injected.build.abi:x86]
複製代碼
其中有一個參數 android.injected.invoked.from.ide=true 代表了是從 AS 調用的 Gradle 命令。後面就是執行 Gradle 的打包命令了。而 Gradle 的打包命令,就是執行各個 Task 生成打包須要的文件。若是對這方面不瞭解,能夠看看以前寫的 Android Gradle Plugin 主要流程分析。
上面介紹了 Android APK 的打包流程,也經過手動打 APK 體驗了整個流程。
在實際的生產開發過程當中,咱們每每會把 APK 發往各個應用市場,不少時候要根據市場渠道進行一些統計,因此就須要對不一樣的市場渠道進行區分。關於多渠道打包的問題,有很多解決方式。
最容易想到的就是使用 Gradle 的 Flavor,使用 Flavor 的特性,生成不一樣的渠道標識。不過這種方式每生成一個渠道包都須要執行一遍構建過程,很是耗時。
另一種方式就是使用 apktool 反編譯 APK,修改其中的資源,添加渠道的標識,並進行從新打包簽名,這種方式省去了構建過程,反編譯,打包,簽名這幾個步驟也比較耗時。按照美團博客的數據,打包 900 個渠道包將近三個小時。
因此須要再尋找其餘的方法。在此以前,咱們先看下 Android APK 簽名的方式。瞭解了簽名方式,才能更好的去了解方法實現。
在瞭解 APK 簽名以前,咱們先看一下 Zip 的文件格式,由於 APK 本質上是一個 Zip 文件,而多渠道打包就涉及到 Zip 的文件格式。
APK 的本質是一個 Zip 文件,咱們能夠用 file 命令看一下。是 V2.0 版本的 Zip 格式文件。
一個 Zip 文件的格式基本以下:
主要能夠分爲三個大區域:
數據區
核心目錄(central directory)
目錄結束標識(end of central directory record,EODR)
這個部分記錄了文件壓縮信息和文件原始數據,每個文件信息用一個 [local file header + file data + data descriptor] 來描述 local file header
記錄了文件的壓縮信息,主要內容以下:
description | length | content |
---|---|---|
local file header signature | 4 bytes (0x04034b50) | 文件頭標識, 0x04034b50 |
version needed to extract | 2 bytes | 解壓所須要的最低版本 |
general purpose bit flag | 2 bytes | 通用標誌位 |
compression method | 2 bytes | 壓縮方式 |
last mod file time | 2 bytes | 文件最後修改時間 |
last mod file date | 2 bytes | 文件最後修改日期 |
crc-32 | 4 bytes | CRC-32 校驗碼 |
compressed size | 4 bytes | 壓縮後文件大小 |
uncompressed size | 4 bytes | 未壓縮文件大小 |
file name length | 2 bytes | 文件名長度 |
extra field length | 2 bytes | 擴展區長度 |
file name (variable size) | 文件名 | |
extra field (variable size) | 擴展數據 |
file data
保存了文件的壓縮數據
data descriptor
description | length | content |
---|---|---|
crc-32 | 4 bytes | crc32 校驗碼 |
compressed size | 4 bytes | 壓縮後文件的大小 |
uncompressed size | 4 bytes | 未壓縮文件的大小 |
表示文件的結束,只有在 local file header 中的 general purpose bit flag 第三 bit 位爲 3 的時候纔會出現。通常狀況下沒有
核心目錄保存了對壓縮文件更多的信息以及對 Zip64 的支持信息。 主要有兩個部分 file header 和 digital signature
file header
是對文件更爲具體的描述,每個 file header 都對應一個 local file header。
description | length | content |
---|---|---|
central file header signature | 4 bytes (0x02014b50) | 核心文件頭標誌,魔數 0x02014b50 |
version made by | 2 bytes | 壓縮使用的版本 |
version needed to extract | 2 bytes | 解壓須要的版本 |
general purpose bit flag | 2 bytes | 通用位標誌 |
compression method | 2 bytes | 壓縮方法 |
last mod file time | 2 bytes | 文件最後修改時間 |
last mod file date | 2 bytes | 文件最後修改日期 |
crc-32 | 4 bytes | crc-32 校驗碼 |
compressed size | 4 bytes | 壓縮後的文件大小 |
uncompressed size | 4 bytes | 壓縮前的文件大小 |
file name length | 2 bytes | 文件名長度 |
extra field length | 2 bytes | 擴展區長度 |
file comment length | 2 bytes | 文件註釋長度 |
disk number start | 2 bytes | 文件在磁盤上的起始位置 |
internal file attributes | 2 bytes | 內部文件屬性 |
external file attributes | 4 bytes | 外部文件屬性 |
relative offset of local header | 4 bytes | 對應的 local file header 的偏移 |
file name (variable size) | 文件名 | |
extra field (variable size) | 擴展數據 | |
file comment (variable size) | 文件註釋 |
Digital signature
官方文檔中沒有對 Digital signature 有太具體的介紹,可是有提到 核心目錄是能夠壓縮和加密的,盲猜是用來加密核心目錄的。
description | length | content |
---|---|---|
header signature | 4 bytes (0x05054b50) | |
size of data | 2 bytes | |
signature data (variable size) |
目錄結束標誌是在整個文件的結尾,用於標記壓縮目錄數據的結束。每一個 Zip 文件有且只有一個結束標誌記錄。
description | length | content |
---|---|---|
end of central dir signature | 4 bytes (0x06054b50) | 目錄結束標記,魔數 0x06054b50 |
number of this disk | 2 bytes | 當前磁盤編號 |
number of the disk with the start of the central directory | 2 bytes | 核心目錄起始位置的磁盤編號 |
total number of entries in the central directory on this disk | 2 bytes | 磁盤上記錄的核心目錄數量 |
total number of entries in the central directory | 2 bytes | 核心目錄結構數量 |
size of the central directory | 4 bytes | 核心目錄大小 |
offset of start of central directory with respect to the starting disk number | 4 bytes | 核心目錄起始位置的偏移 |
.ZIP file comment length | 2 bytes | 註釋長度 |
.ZIP file comment (variable size) | 註釋內容 |
上面基本上就是一個通用的 Zip 包結構了。 下面就看看 Android 的簽名機制。
V1 簽名的機制主要就在 META-INF 目錄下的三個文件,MANIFEST.MF,CERT.SF,CERT.RSA,他們都是 V1 簽名的產物。
MANIFEST.MF 保存了全部文件對應的摘要,部份內容以下:
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 3.4.0
Name: AndroidManifest.xml
SHA-256-Digest: QxJh66y6ssDSNFgZSlf5jIWXfRdWnqL1c3BSwSDUYLQ=
Name: META-INF/android.arch.core_runtime.version
SHA-256-Digest: zFL2eISLgUNzdXtGA4O/YZYOSUPCA3Na3eCjULPlCYk=
複製代碼
CERT.SF 保存了MANIFEST.MF 中每條信息的摘要,部份內容以下:
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA-256-Digest-Manifest: j8YGFgHsujCHud09pT6Igh21XQKSnG+Gqy8VUE55u+g=
X-Android-APK-Signed: 2
Name: AndroidManifest.xml
SHA-256-Digest: qLofC3g32qJ5LmbjO/qeccx2Ie/PPpWSEPBIUPrlKlY=
Name: META-INF/android.arch.core_runtime.version
SHA-256-Digest: I65bgli5vdqHKel7MD74YlSuuyCR/5NDrXr2kf5FigA=
複製代碼
CERT.RSA 包含了對 CERT.SF 文件的簽名以及簽名用到的證書。
在 APK 簽名時,主要流程以下:
計算 APK 中文件摘要,保存摘要的 base64 編碼到 MANIFEST.MF 文件中 ->計算 MANIFEST.MF 文件的摘要,保存其 base64 編碼到 CERT.SF 文件中 -> 計算 MANIFEST.MF 文件中每一個數據塊的摘要,保存其 base64 編碼到 CERT.SF 文件中 -> 計算 CERT.SF 文件摘要,經過開發者私鑰計算數字簽名 -> 保存數字簽名和開發者公鑰到 CERT.RSA 文件中
複製代碼
在 APK 校驗時,主要流程以下:
經過CA 證書解密 CERT.RSA 中的數字證書和對 CERT.SF 的簽名 -> 經過簽名校驗 CERT.SF 文件是否被修改 -> 經過 CERT.SF 驗證 MANIFEST.MF 文件是否被修改 -> 經過 CERT.SF 驗證 MANIFEST.MF 文件中數據項是否被修改 -> 經過 MANIFEST.MF 校驗 APK 中的文件是否被修改
複製代碼
咱們在上面講了 Zip 文件的結構,經過上面校驗過程,咱們能夠發現,在 APK 校驗過程當中,只是校驗了數據區的內容,剩餘的兩個部分沒有作處理。
因此若是修改剩餘兩個部分,簽名校驗過程當中是不會發現的,要寫入信息,EOCD 的註釋字段是很好的選擇。因此將渠道信息寫入 EOCD 的註釋字段,就能夠達到打入渠道信息的目的。這就是騰訊 VasDolly 作的事情。
除此以外,咱們能夠發現,APK 簽名校驗過程當中,並無對 META-INF 文件夾下的文件進行簽名和校驗,因此能夠在 META-INF 文件夾下新增一個空文件,這樣也能夠攜帶渠道信息。這就是美團作的事情。
原本使用上述方案,一切都是很美好的,然而在 Android 7.0 的時候,引入新的簽名方式,APK Signature Scheme v2,致使 V1 上的多渠道簽名方案失效。咱們先看看 V2 簽名的原理。
按照官方文檔來看,V2 簽名是在 數據區和核心目錄區之間,新增了一個 APK Signing Block 區塊,用來記錄簽名信息。
簽名先後的結構以下。
APK Signing Block 格式以下:
description | length | content |
---|---|---|
size of block | 8 bytes | 簽名塊的長度 |
ID-value | 保存了一系列 ID-value | |
size of block | 8 bytes | 簽名塊的長度,和第一個字段同樣 |
magic | 16 bytes | 簽名塊標記,魔數 |
APK Signing Block 中,會對其餘三個模塊都進行簽名,簽名信息保存在 ID-value 中 ID 爲 0x7109871a 對應的 value 中。
在校驗簽名時,會按照下面的邏輯進行。
首先檢查是否包含 V2 簽名塊,若是包含 V2 簽名塊,就採用 V2 簽名進行驗證,若是沒有包含 V2 簽名塊,就採用 V1 簽名驗證。
所以,採用 V2 簽名進行驗證時,V1 方案中添加 EOCD 註釋和 META-INF 空文件的方式就都失效了。
看到這裏,咱們會發現,V2 簽名驗證了其餘三個模塊數據,可是沒有對 APK Signing Block 自己進行驗證,而其中的 ID-value 是一組數據,因此能夠在這裏添加包含渠道信息的 ID-value。這就是 V2 簽名下生成多渠道包的原理。 上述原理具體的代碼,能夠看 github.com/Tencent/Vas… 和 github.com/Meituan-Dia…
在 Android 9.0 中引入了新的簽名方式 V3。爲了解決簽名簽名過時的問題。V3 簽名在 V2 的 APK Signing Block 中新增了一個簽名塊,保存了 supported SDK 版本以及密鑰流轉結構。因爲對 V2 結構沒有進行大的更改,因此不會對多渠道打包方案形成影響。關於 V3 簽名更具體的信息,能夠查看官方文檔。
上面說的 APK 結構和打包,都是完整包含了全部資源,包括適配不一樣的分辨率,適配不一樣的 API 等等,但其實在每個用戶設備上,只會用到其中的一種,其他的資源只會佔用包大小。因此 Google 也提供了一些其餘方法,能夠根據不一樣分辨率,不一樣 ABI 等等來打包,從而減小包大小。
在 Android L 以後,Android 支持了 Multiple APKs,簡單來講就是能夠經過不一樣分辨率,不一樣 CPU 架構,不一樣 API level 把同一個應用打包成對應不一樣設備的多個 APK,而後在不一樣的設備上安裝不一樣的 APK,來減小體積。
文檔裏提供的了詳細的解釋,咱們這裏看一下,經過配置不一樣分辨率,生成的 APK 是什麼樣的。
咱們先在 build.gradle 中添加下面的配置。
android {
...
splits {
// Configures multiple APKs based on screen density.
density {
// Configures multiple APKs based on screen density.
enable true
// Specifies a list of screen densities Gradle should not create multiple APKs for.
exclude "ldpi", "xxhdpi", "xxxhdpi"
// Specifies a list of compatible screen size settings for the manifest.
compatibleScreens 'small', 'normal', 'large', 'xlarge'
}
}
}
複製代碼
含義很清楚了,就很少解釋了。而後咱們編譯,能夠看到,生成的 APK 不是一個,而是四個。
其中 simpleapk-universal-debug.apk 是包含了全部資源的完整 APK。其餘三個 apk 分別是根據不一樣分辨率生成的。其中只包含了對應分辨率須要的資源。從而減小了包大小。咱們能夠對比一下兩個 APK 就看出區別了。
Android L 之後提供了 Split APKs 的多 APK 構建機制,能夠將一個 APK 基於 CPU 架構,屏幕密度等緯度拆分紅多個 APK,當下載應用時,會依據當前的設備配置選擇對應的 APK 安裝。
Split APKs 打包之後,會生成一個 Base APK 和多個 Split APK。
Base APK 包含了通用的資源,而 Split APK 就是用戶設備特有的資源。用戶安裝時會安裝 Base APK 以及其設備對應的 Split APK,從而減小了其餘無用資源的包大小。
咱們正常開發中不會生成 Split APKs,不過咱們能夠打開 Instant Run 對應用進行編譯,如今的 Instant Run 在 Android 5.0 設備上也是採用 Split APKs 的原理。
使用 Instant Run 之後,在 build/intermediates/split-apk/debug/slices/ 下面,能夠看到生成的 Split APKs,咱們選擇其中一個,在 manifest 文件中,定義了 split 標籤,表示當前的 Split APK 編號。
在 build/intermediates/instant-run-apk/debug/ 下面,是 Base APK,包含一些公共的資源。
咱們在安裝 Split APKs,可使用 adb install-multiple 以及 adb shell pm install-create,adb shell pm install-write,adb shell pm install-commit 等命令來進行。具體命令能夠參考這裏。
App Bundle 是 2018 年 Google I/O 大會上引入的新的 App 動態化框架,是藉助 Split APKs 原理完成的動態加載。
和傳統安裝方式對好比下。
App Bundle 依賴於 .aab 文件。一個 aab 的結構以下。
base/ 中是通用的資源,feature1/ feature2/ 中是根據特定屏幕密度,CPU 架構等區分的特定資源。
咱們以 simpleapk demo 爲例看一下 App Bundle 的使用。要使用 App Bundle,須要建立一個 base module 和 feature module。
咱們的 simpleapk 是 base module,而後建立了一個 feature module。這裏要注意的是 feature module 須要選擇 Dynamic Feature Module。
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':simpleapk')
}
複製代碼
而後在 base module 裏配置了 dynamicFeatures。
android {
bundle {
// ...
}
dynamicFeatures = [":dynamic_feature"]
}
複製代碼
咱們在項目中執行 bundleDebug 命令,就能夠看到,在 build/outputs/bunudle/debug 目錄下就生成了 xxx.aab 文件,Google Play 會根據 xxx.aab 生成用戶安裝所須要的 APK 了。
.aab 本質也是一個 zip 文件,咱們解壓看看。包裏的內容以下。
咱們能夠經過 Google 提供的 bundletool 將 .aab 文件生成 APK。執行下面的命令。
java -jar bundletool-all-0.10.2.jar build-apks --bundle=build/outputs/bundle/debug/simpleapk.aab --output=bundle.apks
複製代碼
生成的 bundle.apks 也是一個 zip 文件,咱們也解壓看看。
能夠看到,bundle.apks 裏包含了兩個目錄,一個是 splits/,下面放的是 Base APK 和 Split APK,另外一個目錄是 standalones/,下面放的是根據不一樣屏幕密度生成的完整的 APK,用來在不支持 App Bundle 的設備上進行安裝。
固然如今 App Bundle 的使用,徹底依賴於 Google Play,因此國內仍是沒法使用的。不過對其原理的瞭解,咱們也能知道 Google 是如何區減小包大小的。
pkware.cachefly.net/webdocs/APP… blog.csdn.net/a200710716/…
source.android.com/security/ap… source.android.com/security/ap… zhuanlan.zhihu.com/p/26674427
tech.meituan.com/2014/06/13/… tech.meituan.com/2017/01/13/… developer.android.com/studio/comm…
developer.android.com/studio/buil…
www.infoq.cn/article/2Mg… blog.csdn.net/ximsfei/art…
developer.android.com/guide/app-b… developer.android.com/studio/comm…