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