原文链接 Exploring the Android App Bundle,作者 Joe Birch。
目录
App bundle 格式
拆分 APKs
构建和分发 Android App Bundle
Bundle 工具
动态服务功能
动态提供功能
我实际会节省多少大小?
今年(2018年)在 I/O 上宣布了大量令人兴奋的事情 - 我最关注的一件事是新的 app bundle 格式。 这种格式不仅为我们的应用程序提供了新的上传格式,而且还有助于影响我们以模块化格式构建和结构化应用程序的方式。在这篇文章中,我想深入了解 Android App Bundle,以便我们可以全面了解它是什么,它的工作方式以及我们如何在我们自己的应用程序中添加对它的支持。
正如我上面提到的,我很高兴听到这种称为 App Bundle 的新上传格式。 虽然此 bundle 仍包含我们的应用程序代码和资源文件,但最大的区别在于构建 APKs 的责任转移到了 Google Play 上。 从此,新的 Dynamic Delivery 可用于创建满足用户设备要求的优化了的 APKs,并在运行时传送以进行安装。
但为什么我们要考虑使用 Android App Bundle 呢?
考虑到所有这些,让我们深入了解 app bundle 格式以及围绕它的所有概念吧!
注意:要遵循本文的任何 IDE 部分,您需要运行 Android Studio 3.2。
在我们开始深入了解 app bundle 之前,了解这种格式非常重要。 app bundle 由 zip 存档组成,其中包含组成 bundle 的文件集合。 虽然这些文件是在我们熟悉的 APKs 中找到的文件,但它的目的与 APK 不同。
APK 是我们可以直接向我们的用户设备提供服务的东西,而另一方面,App Bundle 是一种发布格式,无法单独安装到设备上。虽然它们确实有相似之处,但 app bundle 确实包含一些我们在 APKs 中找不到的内容。 例如,bundles 中的元数据文件被用来构建提供给我们用户的 APKs - 这些文件不会包含在 APKs 本身中。 虽然您可能已经熟悉 app bundle 中的大部分内容,但还是让我们来看看典型的 app bundle 可能包含的内容:
这里提到的最后 3 个文件是 App Bundle 的关键部分,因为这些表用于描述我们的应用程序的目标。这是动态传送中的关键概念,因为它用于描述我们所服务的设备和/或用户的类型,这使我们能够根据此信息提供特定的资源构建。
这些 resource,assets 和 native 表都使用我们在应用程序中已经熟悉的信息来向用户提供内容。 我们熟悉使用 drawable-hdpi, lib/armeabi-v7a 或 values-es 等目录来针对特定用户并提供特定资源 - app bundle 在将特定资源定位到用户和设备时使用完全相同的方法。 因此,在组织这些事情时,我们不需要做任何不同的事情。
在 Android lollipop 中,我们看到一个名为 Split APKs 的功能被添加到了平台上。 这允许将多个 APKs 添加到设备,同时仍然表现为他们是单个 app 的一部分。 这些可以作为不同的组合安装在不同的设备上 - 同时仍然作为单个 APK 出现。
这些拆分 APKs 具有与 APK 完全相同的格式,同时彼此共享相同的包名和版本号。 App Bundle 格式用于生成这些拆分 APKs,而这些 APKs 又可以提供给我们的用户设备。 首先,App Bundle 用于分析其所有资源,以查找所有设备配置共有的部分 - 这将是清单文件,dex 文件以及任何其他相同的部分,无论设备,架构还是正在使用的语言环境。这些公共部分将构成我们应用程序的基础 APK,然后,将使用拆分 APKs 为不同类型的配置创建拆分。
然后,将生成配置拆分,以便能够为用户提供满足其设备设置要求的拆分 apks 集合:
因此,当用户使用我们的应用程序时,这个拆分 APKs 的子集将被传送到他们的设备。 这比当前为每个用户提供每个配置的配置更有效,因为大多数用户可能不会使用当前正在传送给他们设备的大量资源。 为了正确看待这一点,下面显示了三种不同的配置,可以提供给三个不同的用户 - 只提供他们需要的资源:
如果用户在任一时刻更改其设备配置(例如添加另一种语言选项),则 play store 将识别这种情况,并尝试为在其设备上使用 split APKs 的所有应用程序下载新的配置拆分。如果设备当时不在线,那么下一次有机会时就会这样做。
现在看来,虽然拆分 APKs 仅在 Lollipop 及更高版本上支持,app bundle 仍然有助于为使用早期版本的 Android 设备节省大小。 不再生成拆分,而是将创建独立的 APKs,以匹配架构和设备密度的不同组合的矩阵。在这种方法中,所有语言都包含在每个 APK 中,因为矩阵会因太多不同的组合而变得太大。对于这些 pre-L 设备,将为给定设备选择最合适的 APK 并将其提供给用户。
正如您从App Bundle 到目前为止所看到的,无论我们支持哪种 SDK 版本,我们都可以在应用程序大小方面节省成本。 这里最棒的是,作为开发人员,我们不需要担心这个过程涉及的任何细节。 我们只需要上传一个 app bundle,Google Play 就会生成正确的 APK 拆分,然后选择要投放到每个设备的正确的 APKs。
在从我们的项目构建 app bundle 时,我们可以直接从 Android Studio 执行此操作。 这可以直接从 Build 菜单中完成,选择 Generate Signed Bundle / APK - 从这里您将看到以下对话框:
此时,我们可以选择构建 Android App Bundle 或 APK。选择这些选项之一将带我们进入 keystore selection/creation 对话框,然后向导将从中构建所需的选择。
如果我们在此处构建 app bundle,则将生成 .aab 文件,这是代表 app bundle 的格式。 除了在 IDE 中使用此向导构建 App Bundle 之外,我们还可以从命令行创建 app bundle - 这对于 CIs 或喜欢使用命令行工作的人很有用。
./gradlew modulename:assemble
./gradlew modulename:assembleVariant
在构建你的 bundle 时,默认情况下会生成所有拆分。 但是在 build.gradle 文件的 android 块中,您可以声明将生成哪些拆分:
bundle {
language {
enableSplit = false
}
density {
enableSplit = true
}
abi {
enableSplit = true
}
}
默认情况下,这些属性将设置为 true。 但是,将一项设置为 false 将意味着不支持指定 bundle 的配置,导致该属性的资源将打包到基本 APK 和所提供的任何动态功能 APK 中。
一旦你构建了你的 App Bundle,就可以将其简单地上传到 play 控制台。 为了能够通过 Play Strore 分发 app bundles,您首先需要注册 Google Play 应用程序签名。 因为工具会为您生成不同的 APK 拆分,所以它还需要为它们签名的功能 - 这是必需的,如果没有它,您将无法使用 app bundles。
现在我们已经通过 Android Studio(或命令行)构建了我们的 app bundle,我们可以将其上传到 Play Console 以准备分发。 如果您直接进入发布页面,就像您对 APK 做的一样,您会注意到您还可以通过相同的上传区域上传 App Bundle:
上传 bundle 后,您将能够看到 Android App Bundle 已添加到上传区域下方的组件列表中。 展开我们刚上传的 Android App Bundle 将向您显示 APK 所做的所有信息,除了这次我们可以看到一个 Explore App Bundle 按钮:
我上传 bundle 的 Application 仅支持单个区域设置(仅作为示例),但确实支持一系列不同的屏幕密度。 因此,App Bundle Explorer 向我们显示了将从给定 app bundle 中提供的不同设备配置 APKs 的细分。从这里我们还可以下载不同的 APK 用于测试目的,以及查看每个 APK 配置将提供给的设备。
您会注意到,在顶部,我们还显示了使用 App Bundle 格式而非标准 APK 上传方法的大小节省值。这在应用程序之间会有所不同,因为这只是一个示例应用程序,节省的成本可能无法真正代表您在自己的应用程序所看到的内容。
现在,在将 App Bundle 上传到 Google Play Store 之前,对 bundle 进行一些测试非常重要。虽然我们可以使用内部测试跟踪来执行此操作,但我们也可以在本地对其进行全面测试,以确保一切按预期工作。为此我们可以使用 bundletool ,这将使我们的这个过程非常简单。 我们的 IDE 和 Google Play 使用此工具来构建我们的 bundle 以及将其转换为不同的配置拆分,因此我们将在本地看到的是用户将获得的服务的真实表示。
当我们运行 bundle 工具时,它将根据我们的应用程序配置生成一组 APKs。为了解决这个问题,让我们为 bundle 支持的所有不同配置创建一组 unsignedAPKs :
bundletool build-apks --bundle=/Users/joebirch/releases/sample.aab --
output=/Users/joebirch/releases/sample.apks
注意:如果您希望运行相同的任务但生成签名的 APKs,则可以通过将 keystore 信息添加到命令来执行此操作:
bundletool build-apks --bundle=/Users/joebirch/releases/sample.aab --output=/Users/joebirch/releases/sample.apks
--ks=/Users/joebirch/some_keystore.jks
--ks-pass=file:/Users/joebirch/some_keystore.pwd
--ks-key-alias=SomeAlias
--key-pass=file:/Users/joebirch/some_key.pwd
现在我们已经生成了这些 APKs,我们希望将它们提供给本地设备- bundetool 可以为我们完成这项工作。
bundletool install-apks --apks=/Users/joebirch/releases/sample.apks
假设我们连接的设备至少运行 Android 5.0。当我们运行此命令时,Bundletool 会将 base APK 以及任何动态功能和配置 APKs 推送到特定于该设备配置的设备 - 与 Play Store 给用户提供应用程序的方式相同。如果我们连接不同的设备,例如使用不同的密度/区域设置,则会向该设备提供一组不同的配置APKs。如果连接的设备在 Android 5.0 下运行,则最适合的 multi-APK 将被安装到设备上。处理多个设备时,您可以使用 --device-id=serial-id
来描述应用程序应该安装到那个设备。
当bundletool 为连接的设备生成已安装的内容时,您可以检索设备规范 JSON 格式。然后,可以使用它从生成的 APKs 中提取特定的APK。首先,我们需要运行命令:
bundletool get-device-spec
现在我们已经为特定的设备配置提供了这个 JSON 文件,我们可以继续使用额外的特定配置来拆分:
bundletool extract-apks
--apks=/Users/joebirch/releases/someApkSet.apks
--output-dir=/Users/joebirch/releases/device_APK_set.apks
--device-spec=/Users/joebirch/releases/some_configuraton.json
您还可以手动创建自己的设备配置 JSON 文件,并用它来提取特定配置的 APK。这对于测试您可能无法专门访问的设备配置非常有用。
{
"supportedAbis": ["arm64-v8a"],
"supportedLocales": ["en", "es"],
"screenDensity": 640,
"sdkVersion": 21
}
App Bundle 的另一个关键部分就是所谓的 Dynamic Delivery。此功能允许您定义首次安装应用程序时可能不需要的模块。在我们的项目中,我们可以定义这些模块,然后在需要时使用新的 Play Core Library 按需安装。这可能是一项并非您的应用程序的所有用户都可以使用的功能,或者可能不是核心功能。 同样,这可以从初始下载中节省大小,Google Play 可以在需要时为我们提供 Dynamic Feature。
为了能够添加对此的支持,让我们快速运行一下在应用程序中创建 Dynamic Feature Module。我们可以通过右键单击项目的根模块并选择 New Module 来转到 Create New Module 向导来开始。从这里我们可以选择 Dynamic Feature Module 并点击 Next。
现在我们需要选择应用程序的 Base 模块 - 这将是此 Dynamic Feature Module 将依赖的可安装模块。填写完所需信息后,系统将提示我们命名模块,然后我们可以完成 Dynamic Feature 模块将被添加到的位置的设置,并将其添加到我们的应用程序中。
一旦你创建了这个模块,那值得快速浏览一下。这是为了让您了解 Dynamic Feature Module 的配置差异,了解工作原理总是很好的,如果您将来需要转换模块,那么您就知道需要添加哪些信息。
如果打开 module build.gradle 文件,则会注意到使用的插件不同:
apply plugin: 'com.android.dynamic-feature'
与我们可能看到的用于 instant apps 的功能模块略有不同,此插件是您的模块被归类为动态功能模块所必需的。 同时在此文件中您应该知道 dynamic-feature build.gradle 文件没有使用的一些属性 - 例如,versionCode,vesionName,minification 和签名属性都取自基本模块。
接下来,如果打开您的基础 modules build.gradle 文件,您会注意到我们的 dynamic feature 已添加到一个 dynamicFeatures 属性数组中:
dynamicFeatures = [":first-dynamic-feature"]
这是声明可用于您的应用程序的动态功能,每当您添加对新动态功能的支持时,都必须更新这些功能,以便您的 base module 了解它们。最后,如果打开动态功能模块,则可以打开该模块的清单文件以查找以下内容:
现在我们已经为应用程序添加了 Dynamic Feature Module,我们希望在用户请求时将其实际地提供给用户。为此,我们将使用 Play Core library,它为我们提供了这样做的功能。该库允许用户打算与可能尚未安装的功能进行交互,此时我们的应用程序将请求该功能,下载该功能,然后在安装后处理其状态。
要开始使用此功能,我们需要首先通过依赖项将 play 核心库添加到项目中:
implementation 'com.google.android.play:core:1.3.4'
现在我们可以使用这个库了,我们需要在适当的地方使用它。要在运行时下载动态功能,我们将使用 SplitInstallManager 类。同时我们的应用程序位于前台,这将用于请求动态功能,然后下载以进行安装。
val splitInstallManager = SplitInstallManagerFactory.create(this)
现在我们有了这个类,我们将创建一个 SplitInstallRequest 实例来下载我们的模块:
val request = SplitInstallRequest
.newBuilder()
.addModule("someDynamicModule")
.build()
此实例将包含将用于从 Google Play 请求我们的动态功能模块的请求信息。在该请求中,我们可以声明我们想要请求的多个模块,这可以通过简单地链式调用 request builder 的多个 addModule() 来完成。
最后,我们将使用 install manager 实例来运行我们刚刚创建的请求。 这里我们将在 manager 上使用 startInstall() 函数,传入我们之前创建的请求,并在安装完成,成功或失败时添加回调,以便相应地处理 UI。
splitInstallManager
.startInstall(request)
.addOnSuccessListener { }
.addOnFailureListener { }
.addOnCompleteListener { }
这里的 stateInstall 函数调用将尽快触发安装过程。但是,如果您希望推迟应用程序已经后台运行的安装过程,则可以使用 deferInstall() 调用来替代执行此操作。
当您调用这些函数中的任何一个时,将返回一个 Int 值,该值表示拆分安装的会话 ID。如果在请求安装期间的任何时候你想取消它,那么你可以通过调用 cancelInstall() 函数,传入相应请求的会话 ID 来实现。
在安装过程中,可能会出现一系列不同的错误,让我们快速了解一下这些可能是什么错误:
现在,当此请求发生时,我们可能会使用任何形式的UI覆盖,例如 Billing Library 或其他 Google Play 集成。因此,在下载和安装动态功能时,用户了解应用程序中发生的情况是非常重要的 - 为此,我们可以使用SplitInstallStateUpdatedListener ,这将允许我们监视请求的状态。
val stateListener = SplitInstallStateUpdatedListener { state ->
when (state.status()) {
PENDING -> { }
DOWNLOADING -> { }
DOWNLOADED -> { }
INSTALLED -> { }
INSTALLING -> { }
REQUIRES_USER_CONFIRMATION -> { }
FAILED -> { }
CANCELING -> { }
CANCELED -> { }
}
}
splitInstallManager.registerListener(stateListener)
在这,您可以看到正在被定义的侦听器的实例,以及安装可能处于的不同状态 - 您应该在应用程序中使用它来处理 UI。例如,您可能希望显示某种形式的进度条以让用户知道这些状态正在发生,但是当进程通过每个状态传播时,要更改使用的消息。
拆分安装可以包含一系列不同的状态,让我们快速查看它们都是什么:
一旦安装了动态功能模块,仍然可以执行一些操作来管理它们。 例如,我们可以使用 deferredUninstall() 函数卸载模块 - 传入一些我们要从应用程序的用户安装中删除的模块名称。
splitInstallManager
.deferredUninstall(listOf("someDynamicModule"))
.addOnSuccessListener { }
.addOnFailureListener { }
.addOnCompleteListener { }
我们还可以使用 manager 实例上的 getInstalledModules() 函数检索已安装模块名称的列表,这对于在应用程序中设置任何 UI 并在执行任何删除请求之前检查模块安装状态非常有用。
val installedModules = splitInstallManager.installedModules
现在,您将节省应用程序的大小这句话说得很好,但是对于您将实际节省的内容,有一些指导是很有帮助的。据 Google 称,平均而言,使用 App Bundle 格式的应用程序的大小要小 20%-这意味着每次下载或更新应用程序时,所涉及的数据传输都要少 20%。
Google 还对 Play Store 中至少有 100 万下载量的所有应用程序进行了一些分析,他们发现:
将所有这些组合在一起后,发现如果所有这些应用程序都使用 App Bundle 格式,则每天将节省 10PB 的数据。 这是一个非常高的数据量!
Google 还从 app bundle 格式的早期采用者那里分享了一些样本数据。例如,虽然 Twitter for Android 之前已经提供过 multi-APKs,但 app bundles 能让他们看到约 20% 的大小减少。该应用程序支持许多不同的语言和密度,这是他们的大量节省的来源。App Bundle 的使用还意味着他们不再需要为他们希望支持的配置手动创建和上传单独的 APKs,因为 App Bundle 工具将自动处理此问题。
另一方面,Text Plus 应用程序不支持用于不同的配置的多个 APKs。该应用程序在不同的密度和架构方面拥有大量资源,因此 App Bundle 能让它们为其应用程序节省大约 26%。
最后,当团队添加对 App Bundle 的支持时,Jamo 应用程序能够将其原始应用程序大小减半。该应用程序使用许多不同的大型 native 库来支持不同的架构 - app bundle 现在允许它们优化这些要求,以便可以向用户提供较小的 APKs。
我希望通过阅读本文,您能够了解什么是 App Bundles,它们是如何工作的,以及如何将其与 Dynamic Delivery 结合起来,以改进我们为用户提供应用程序的方法。我很高兴在生产中使用 App Bundles,并进一步了解如何使用它们来改进我们的应用程序大小和传送。