App Bundle(.aab) 作为 Android 官方力推的新交付格式已经存在了一段时间,而今年 PlayStore 的政策强制“所有新应用必须使用 .aab”进行提交也成为了大家转换过去的一大动力。兼容和打包 .aab 文件格式其实并不复杂,简单添加对应的 DSL 配置并替换掉执行的打包命令为 bundle${VariantName}
即可。但是,打包后得到的 .aab 并不能直接在本地直接使用 adb 安装到调试设备,需要借助 bundletool 转换到 .apks 后再调用其安装命令进行安装。
BundleTool 官方文档 列举了 CLI 命令的各类用途;放到更复杂的环境中,你可能碰到过 BundleTool 的这些场景:
这些场景常常伴随着下列的一些问题:
不禁思考,AGP 有 BundleTool 的依赖,没有这些 BundleTool 后续转换的 Task 吗?使用 Gradle 来做这些后续的操作其实更适合?
如果我们打开 IDE 的 Gradle Task 列表,查询 “Bundle“ 关键词,很容易就会发现 makeApkFromBundleForXxxx
等任务,它们的实现是 com.android.build.gradle.internal.tasks.BundleToApkTask
这个类。
在源码中查看该类的关联使用,你会发现除了注册没有任何地方有该类的使用痕迹。而这个任务自身的具体作用其实只是:
PackageXxxBundle
,等它打出一个未签名的 Bundle;BuildApksCommand
命令打出一个 .apks 包。enableLocalTesting
,其余的都使用 BundleTool build-apks
命令的默认值;/intermediates
文件夹中),而非最终产物。这就有点食之无味、弃之可惜了:有现成的 Task 但使用范围十分局限。不如...咱借助 BundleTool 库自己封装一个 Gradle Plugin?
简单分析下 BundleTool 的几个命令,我们发现它们的依赖关系如下:
其中:
build-apks
是其他几个命令的必选前置任务(实线),get-device-sepc
是几个命令的可选前置任务(虚线)。build-apks
和 get-size
(斜体部分)和构建流程关系紧密,不需要测试设备参与;其余命令需要测试设备参与。get-device-spec
导出 json 文件是一次性的任务,我们可以假设这部分已经完成;如此,我们涉及的领域也清楚了:build-apks
和 get-size
等和最终产物直接相关的命令。install-apks
和 extract-apks
在本地测试可以根据当前设备使用 CLI 完成,在 CI 或者云真机测试平台等一般有专用的脚本去结合 BundleTool 处理。
然后我们考虑获取如何获取最终输出的 Bundle 并修改。新版 Variant API 其实已经提供了方便修改和获取最终 Bundle 的方法,整个流程可以参考如下的运行截图。可以看到对比之前的中间产物模式,Variant API 的产物都已经输出到 /outputs
文件夹了。由于我们不需要中间 aab,所以我们只要简单调用 variants.get(SingleArtifact.BUNDLE)
,把获取的 .aab 文件传入自定义 Task,之后再借鉴 AGP 的代码包装下各类命令即可:
// 一个验证想法的简单 Task,完整插件的实现比这个复杂一些,请直接参考文末的仓库链接
abstract class ConsumeBundleFileTask : DefaultTask() {
@get:InputFiles
abstract val finalBundleProperty: RegularFileProperty
@get:Internal
abstract val buildToolInfo: Property<BuildToolInfo>
@get:Nested
lateinit var signingConfigData: SigningConfigDataProvider
@get:OutputFile
abstract val apksFileProperty: RegularFileProperty
@TaskAction
fun taskAction() {
val aapt2Path = buildToolInfo.get().getPath(BuildToolInfo.PathId.AAPT2)
println(".get(SingleArtifact.BUNDLE)")
println("[ConsumeBundleFileTask][input]:" + finalBundleProperty.asFile.get().absolutePath)
println("[ConsumeBundleFileTask][output]:" + apksFileProperty.asFile.get().absolutePath)
val signingConfigData = signingConfigData.resolve()!!
val command = BuildApksCommand.builder()
.setBundlePath(finalBundleProperty.asFile.get().toPath())
.setOutputFile(apksFileProperty.asFile.get().toPath())
.setAapt2Command(
Aapt2Command.createFromExecutablePath(
File(aapt2Path).toPath()
)
)
.setSigningConfiguration2(
keystoreFile = signingConfigData.storeFile,
keystorePassword = signingConfigData.storePassword,
keyAlias = signingConfigData.keyAlias,
keyPassword = signingConfigData.keyPassword
).setLocalTestingMode(false)
command.build().execute()
}
...
}
不了解新版 Variant API 的朋友可以参考我这个月在 GDG 社区的分享《扩展 Android 构建流程 - 基于新版 Variant/Artifact APIs》(回放地址)。
最后我们简单看下 BundleTool 的 BuildApksCommand.Builder,这个 Builder 的 setXxx 相关的 API 过去一年也就两个小改动,其中还有一个是新增方法,不太影响原有的兼容性,相比 AGP 来说整体相对稳定了。
至此,整个插件的理论构建成本和维护成本都在可接受范围内。
插件的开发和我之前写过的几个并无差别,我们直接来看插件的使用。
0x01. Add the plugin to classpath:
buildscript {
repositories {
...
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.4")
classpath("me.2bab:bundle-tool-plugin:1.1.0")
}
}
0x02. Apply Plugin:
// For your application module
plugins {
id("me.2bab.bundletool")
}
0x03. Advanced Configurations
import me.xx2bab.bundletool.*
bundleTool {
// 这里是一个很有趣的配置项,它可以按不同 variant 渠道去
// 开启插件的几个功能特性,例如这里我们把 debug + Get_SIZE 功能的组合禁掉了。
// 你可以根据项目实际的 buildtype 和 flavor 去调整和开启需要的功能。
enableByVariant { variant, feature ->
!(variant.name.contains("debug", true) && feature == BundleToolFeature.GET_SIZE)
}
// 每个配置项会对应到一个 `build-apks` 命令的执行
buildApks {
create("universal") {
buildMode.set(ApkBuildMode.UNIVERSAL.name)
}
create("pixel4a") {
deviceSpec.set(file("./pixel4a.json"))
}
}
// 每个配置项都会依次计算上面 `buildApks` 所有输出的 apks 的大小,
// 按当前的配置会输出 2 * 1 = 2 份 csv 文件
getSize {
create("all") {
dimensions.addAll(
GetSizeDimension.SDK.name,
GetSizeDimension.ABI.name,
GetSizeDimension.SCREEN_DENSITY.name,
GetSizeDimension.LANGUAGE.name)
}
}
}
0x04. Build your App and Enjoy!
# 确保执行命令里的 Variant 是 `enableByVariant` 中允许的
./gradlew TransformApksFromBundleForProductionRelease
最后可以在 /app/outputs/bundle/${variantName}/bundletool
中找到输出的结果。
希望这个小工具可以帮助大家在集成新的 Android Bundle 时提供一些帮助,插件已经开源到我 Github:bundle-tool-gradle-plugin。关于按 Variant 开关插件功能的思路,请参考下一篇构建指北#12。
欢迎关注我的 Github / 公众号 / 播客 / 微博 / Twitter。