This document describes how the Chromium build system supports Android app bundles.
[TOC]
An Android app bundle is an alternative application distribution format for Android applications on the Google Play Store, that allows reducing the size of binaries sent for installation to individual devices that run on Android L and beyond. For more information about them, see the official Android documentation.
For the context of this document, the most important points are:
-
Unlike a regular APK (e.g.
foo.apk
), the bundle (e.g.foo.aab
) cannot be installed directly on a device. -
Instead, it must be processed into a set of installable split APKs, which are stored inside a special zip archive (e.g.
foo.apks
). -
The splitting can be based on various criteria: e.g. language or screen density for resources, or cpu ABI for native code.
-
The bundle also uses the notion of dynamic features modules (DFMs) to separate several application features. Each module has its own code, assets and resources, and can be installed separately from the rest of the application if needed.
-
The main application itself is stored in the '
base
' module (this name cannot be changed).
Here's an example that shows how to declare a simple bundle that contains a single base module, which enables language-based splits:
# First declare the first bundle module. The base module is the one
# that contains the main application's code, resources and assets.
android_app_bundle_module("foo_base_module") {
# Declaration are similar to android_apk here.
...
}
# Second, declare the bundle itself.
android_app_bundle("foo_bundle") {
# Indicate the base module to use for this bundle
base_module_target = ":foo_base_module"
# The name of our bundle file (without any suffix). Default would
# be 'foo_bundle' otherwise.
bundle_name = "FooBundle"
# Enable language-based splits for this bundle. Which means that
# resources and assets specific to a given language will be placed
# into their own split APK in the final .apks archive.
enable_language_splits = true
# Proguard settings must be passed at the bundle, not module, target.
proguard_enabled = !is_java_debug
}
When generating the foo_bundle
target with Ninja, you will end up with
the following:
-
The bundle file under
out/Release/apks/FooBundle.aab
-
A helper script called
out/Release/bin/foo_bundle
, which can be used to install / launch / uninstall the bundle on local devices.This works like an APK wrapper script (e.g.
foo_apk
). Use--help
to see all possible commands supported by the script.
Please see Dynamic Feature Modules for more details. In short, if you need more modules besides the base one, you will need to list all the extra ones using the extra_modules variable which takes a list of GN scopes, as in:
android_app_bundle_module("foo_base_module") {
...
}
android_app_bundle_module("foo_extra_module") {
...
}
android_app_bundle("foo_bundle") {
base_module_target = ":foo_base_module"
extra_modules = [
{ # NOTE: Scopes require one field per line, and no comma separators.
name = "my_module"
module_target = ":foo_extra_module"
}
]
...
}
Note that each extra module is identified by a unique name, which cannot
be 'base
'.
Signing an app bundle is not necessary, unless you want to upload it to the
Play Store. Since this process is very slow (it uses jarsigner
instead of
the much faster apkbuilder
), you can control it with the sign_bundle
variable, as described in the example above.
The .apks
archive however always contains signed split APKs. The keystore
path/password/alias being used are the default ones, unless you use custom
values when declaring the bundle itself, as in:
android_app_bundle("foo_bundle") {
...
keystore_path = "//path/to/keystore"
keystore_password = "K3y$t0Re-Pa$$w0rd"
keystore_name = "my-signing-key-name"
}
These values are not stored in the bundle itself, but in the wrapper script,
which will use them to generate the .apks
archive for you. This allows you
to properly install updates on top of existing applications on any device.
When using an app bundle that is made of several modules, it is crucial to ensure that proguard, if enabled:
- Keeps the obfuscated class names used by each module consistent.
- Does not remove classes that are not used in one module, but referenced by others.
To achieve this, a special scheme called synchronized proguarding is performed, which consists of the following steps:
-
The list of unoptimized .jar files from all modules are sent to a single proguard command. This generates a new temporary optimized group .jar file.
-
Each module extracts the optimized class files from the optimized group .jar file, to generate its own, module-specific, optimized .jar.
-
Each module-specific optimized .jar is then sent to dex generation.
This synchronized proguarding step is added by the android_app_bundle()
GN
template. In practice this means the following:
-
proguard_enabled
must be passed toandroid_app_bundle
targets, but not toandroid_app_bundle_module
ones. -
proguard_configs
can be still passed to individual modules, just like regular APKs. All proguard configs will be merged during the synchronized proguard step.
Note that the foo_bundle
script knows how to generate the .apks archive
from the bundle file, and install it to local devices for you. For example,
to install and launch a bundle, use:
out/Release/bin/foo_bundle run
If you want to manually look or use the .apks
archive, use the following
command to generate it:
out/Release/bin/foo_bundle build-bundle-apks \
--output-apks=/tmp/BundleFoo.apks
All split APKs within the archive will be properly signed. And you will be
able to look at its content (with unzip -l
), or install it manually with:
build/android/gyp/bundletool.py install-apks \
--apks=/tmp/BundleFoo.apks \
--adb=$(which adb)
The task of examining the manifest is simplified by running the following, which dumps the application manifest as XML to stdout:
build/android/gyp/bundletool.py dump-manifest