blob: b5cc59fb08cd2c0e055038ed00f1384fb04a24e5 [file] [log] [blame] [view]
fweacae1cd2022-02-17 09:45:38 -08001Project: /_project.yaml
2Book: /_book.yaml
3
4# bazel mobile-install
5
Googler3b9ed6e2022-11-08 02:19:42 -08006{% include "_buttons.html" %}
7
fweacae1cd2022-02-17 09:45:38 -08008<p class="lead">Fast iterative development for Android</p>
9
10This page describes how `bazel mobile-install` makes iterative development
11for Android much faster. It describes the benefits of this approach versus the
12challenges of the traditional app install method.
13
14## Summary {:#summary}
15
16To install small changes to an Android app very quickly, do the following:
17
18 1. Find the `android_binary` rule of the app you want to install.
19 2. Disable Proguard by removing the `proguard_specs` attribute.
20 3. Set the `multidex` attribute to `native`.
21 4. Set the `dex_shards` attribute to `10`.
22 5. Connect your device running ART (not Dalvik) over USB and enable USB
23 debugging on it.
24 6. Run `bazel mobile-install :your_target`. App startup will be a little
25 slower than usual.
26 7. Edit the code or Android resources.
27 8. Run `bazel mobile-install --incremental :your_target`.
28 9. Enjoy not having to wait a lot.
29
30Some command line options to Bazel that may be useful:
31
32 - `--adb` tells Bazel which adb binary to use
33 - `--adb_arg` can be used to add extra arguments to the command line of `adb`.
34 One useful application of this is to select which device you want to install
35 to if you have multiple devices connected to your workstation:
36 `bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target`
37 - `--start_app` automatically starts the app
38
39When in doubt, look at the
40[example](https://github.com/bazelbuild/bazel/tree/master/examples/android){: .external}
41or [contact us](https://groups.google.com/forum/#!forum/bazel-discuss){: .external}.
42
43## Introduction {:#introduction}
44
45One of the most important attributes of a developer's toolchain is speed: there
46is a world of difference between changing the code and seeing it run within a
47second and having to wait minutes, sometimes hours, before you get any feedback
48on whether your changes do what you expect them to.
49
50Unfortunately, the traditional Android toolchain for building an .apk entails
51many monolithic, sequential steps and all of these have to be done in order to
52build an Android app. At Google, waiting five minutes to build a single-line
53change was not unusual on larger projects like Google Maps.
54
55`bazel mobile-install` makes iterative development for Android much faster by
56using a combination of change pruning, work sharding, and clever manipulation of
57Android internals, all without changing any of your app's code.
58
59## Problems with traditional app installation {:#problems-app-install}
60
61Building an Android app has some issues, including:
62
63- Dexing. By default, "dx" is invoked exactly once in the build and it does not
64know how to reuse work from previous builds: it dexes every method again, even
65though only one method was changed.
66
67- Uploading data to the device. adb does not use the full bandwidth of a USB 2.0
68connection, and larger apps can take a lot of time to upload. The entire app is
69uploaded, even if only small parts have changed, for example, a resource or a
70single method, so this can be a major bottleneck.
71
72- Compilation to native code. Android L introduced ART, a new Android runtime,
73which compiles apps ahead-of-time rather than compiling them just-in-time like
74Dalvik. This makes apps much faster at the cost of longer installation
75time. This is a good tradeoff for users because they typically install an app
76once and use it many times, but results in slower development where an app is
77installed many times and each version is run at most a handful of times.
78
79## The approach of `bazel mobile-install` {:#approach-mobile-install}
80
81`bazel mobile-install `makes the following improvements:
82
83 - Sharded dexing. After building the app's Java code, Bazel shards the class
84 files into approximately equal-sized parts and invokes `dx` separately on
85 them. `dx` is not invoked on shards that did not change since the last build.
86
87 - Incremental file transfer. Android resources, .dex files, and native
88 libraries are removed from the main .apk and are stored in under a separate
89 mobile-install directory. This makes it possible to update code and Android
90 resources independently without reinstalling the whole app. Thus,
91 transferring the files takes less time and only the .dex files that have
92 changed are recompiled on-device.
93
94 - Loading parts of the app from outside the .apk. A tiny stub application is
95 put into the .apk that loads Android resources, Java code and native code
96 from the on-device mobile-install directory, then transfers control to the
97 actual app. This is all transparent to the app, except in a few corner cases
98 described below.
99
100### Sharded Dexing {:#sharded-dexing}
101
102Sharded dexing is reasonably straightforward: once the .jar files are built, a
103[tool](https://github.com/bazelbuild/bazel/blob/master/src/tools/android/java/com/google/devtools/build/android/ziputils/DexMapper.java){: .external}
104shards them into separate .jar files of approximately equal size, then invokes
105`dx` on those that were changed since the previous build. The logic that
106determines which shards to dex is not specific to Android: it just uses the
107general change pruning algorithm of Bazel.
108
109The first version of the sharding algorithm simply ordered the .class files
110alphabetically, then cut the list up into equal-sized parts, but this proved to
111be suboptimal: if a class was added or removed (even a nested or an anonymous
112one), it would cause all the classes alphabetically after it to shift by one,
113resulting in dexing those shards again. Thus, it was decided to shard Java
114packages rather than individual classes. Of course, this still results in
115dexing many shards if a new package is added or removed, but that is much less
116frequent than adding or removing a single class.
117
118The number of shards is controlled by the BUILD file (using the
119`android_binary.dex_shards` attribute). In an ideal world, Bazel would
120automatically determine how many shards are best, but Bazel currently must know
121the set of actions (for example, commands to be executed during the build) before
122executing any of them, so it cannot determine the optimal number of shards
123because it doesn't know how many Java classes there will eventually be in the
124app. Generally speaking, the more shards, the faster the build and the
125installation will be, but the slower app startup becomes, because the dynamic
126linker has to do more work. The sweet spot is usually between 10 and 50 shards.
127
128### Incremental file transfer {:#incremental-file-transfer}
129
130After building the app, the next step is to install it, preferably with the
131least effort possible. Installation consists of the following steps:
132
133 1. Installing the .apk (typically using `adb install`)
134 2. Uploading the .dex files, Android resources, and native libraries to the
135 mobile-install directory
136
137There is not much incrementality in the first step: the app is either installed
138or not. Bazel currently relies on the user to indicate if it should do this step
139through the `--incremental` command line option because it cannot determine in
140all cases if it is necessary.
141
142In the second step, the app's files from the build are compared to an on-device
143manifest file that lists which app files are on the device and their
144checksums. Any new files are uploaded to the device, any files that have changed
145are updated, and any files that have been removed are deleted from the
146device. If the manifest is not present, it is assumed that every file needs to
147be uploaded.
148
149Note that it is possible to fool the incremental installation algorithm by
150changing a file on the device, but not its checksum in the manifest. This could
151have been safeguarded against by computing the checksum of the files on the
152device, but this was deemed to be not worth the increase in installation time.
153
154### The Stub application {:#stub-app}
155
156The stub application is where the magic to load the dexes, native code and
157Android resources from the on-device `mobile-install` directory happens.
158
159The actual loading is implemented by subclassing `BaseDexClassLoader` and is a
160reasonably well-documented technique. This happens before any of the app's
161classes are loaded, so that any application classes that are in the apk can be
162placed in the on-device `mobile-install` directory so that they can be updated
163without `adb install`.
164
165This needs to happen before any of the
166classes of the app are loaded, so that no application class needs to be in the
167.apk which would mean that changes to those classes would require a full
168re-install.
169
170This is accomplished by replacing the `Application` class specified in
171`AndroidManifest.xml` with the
172[stub application](https://github.com/bazelbuild/bazel/blob/master/src/tools/android/java/com/google/devtools/build/android/incrementaldeployment/StubApplication.java){: .external}. This
173takes control when the app is started, and tweaks the class loader and the
174resource manager appropriately at the earliest moment (its constructor) using
175Java reflection on the internals of the Android framework.
176
177Another thing the stub application does is to copy the native libraries
178installed by mobile-install to another location. This is necessary because the
179dynamic linker needs the `X` bit to be set on the files, which is not possible to
180do for any location accessible by a non-root `adb`.
181
182Once all these things are done, the stub application then instantiates the
183actual `Application` class, changing all references to itself to the actual
184application within the Android framework.
185
186## Results {:#results}
187
188### Performance {:#performance}
189
190In general, `bazel mobile-install` results in a 4x to 10x speedup of building
191and installing large apps after a small change.
192
193The following numbers were computed for a few Google products:
194
fwebc2baaa2022-02-22 12:56:16 -0800195<img src="/docs/images/mobile-install-performance.svg"/>
fweacae1cd2022-02-17 09:45:38 -0800196
197This, of course, depends on the nature of the change: recompilation after
198changing a base library takes more time.
199
200### Limitations {:#limitations}
201
202The tricks the stub application plays don't work in every case.
203The following cases highlight where it does not work as expected:
204
205 - When `Context` is cast to the `Application` class in
206 `ContentProvider#onCreate()`. This method is called during application
207 startup before we have a chance to replace the instance of the `Application`
208 class, therefore, `ContentProvider` will still reference the stub application
209 instead of the real one. Arguably, this is not a bug since you are not
210 supposed to downcast `Context` like this, but this seems to happen in a few
211 apps at Google.
212
213 - Resources installed by `bazel mobile-install` are only available from within
214 the app. If resources are accessed by other apps via
215 `PackageManager#getApplicationResources()`, these resources will be from the
216 last non-incremental install.
217
218 - Devices that aren't running ART. While the stub application works well on
219 Froyo and later, Dalvik has a bug that makes it think that the app is
220 incorrect if its code is distributed over multiple .dex files in certain
221 cases, for example, when Java annotations are used in a
222 [specific](https://code.google.com/p/android/issues/detail?id=78144){: .external}
223 way. As long as your app doesn't tickle these bugs, it should work with Dalvik,
224 too (note, however, that support for old Android versions isn't exactly our
225 focus)