这是【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/ 目录下。markdown
开始以前,咱们先建立一个 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…