针对 Android 的快速迭代开发
本页介绍了 bazel mobile-install
如何大大加快 Android 迭代开发速度。该文档介绍了与传统应用安装方法相比,这种方法的优势。
摘要
如需非常快速地安装对 Android 应用所做的细微更改,请执行以下操作:
- 找到要安装的应用的
android_binary
规则。 - 移除
proguard_specs
属性以停用 Proguard。 - 将
multidex
属性设置为native
。 - 将
dex_shards
属性设置为10
。 - 通过 USB 连接运行 ART(而非 Dalvik)的设备,并在其上启用 USB 调试。
- 运行
bazel mobile-install :your_target
。应用启动速度会比平时稍慢。 - 修改代码或 Android 资源。
- 运行
bazel mobile-install --incremental :your_target
。 - 享受不必等待太久的便利。
以下是一些可能对您有用的 Bazel 命令行选项:
--adb
告知 Bazel 要使用哪个 adb 二进制文件--adb_arg
可用于向adb
的命令行添加额外的参数。这项功能的一个实用应用是,如果有多个设备连接到您的工作站,您可以选择要安装到哪个设备上:bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target
- “
--start_app
”会自动启动应用
简介
开发者工具链最重要的属性之一就是速度:更改代码并在 1 秒内看到其运行,与必须等待几分钟(有时甚至几小时)才能获得有关所做更改是否达到预期效果的任何反馈,这两者相差甚远。
遗憾的是,用于构建 .apk 的传统 Android 工具链包含许多单体式顺序步骤,必须完成所有这些步骤才能构建 Android 应用。在 Google 内部,对于 Google 地图等大型项目,等待五分钟来构建一行更改并不罕见。
bazel mobile-install
通过组合使用更改修剪、工作分片和对 Android 内部机制的巧妙操作,可大大加快 Android 的迭代开发速度,而无需更改应用的任何代码。
与传统应用安装相关的问题
构建 Android 应用时会遇到一些问题,包括:
Dexing。默认情况下,“dx”在 build 中仅调用一次,并且它不知道如何重复使用之前 build 中的工作:它会再次对每个方法进行 dex,即使只有一个方法发生了更改也是如此。
将数据上传到设备。adb 不会使用 USB 2.0 连接的全部带宽,较大的应用可能需要很长时间才能上传。系统会上传整个应用,即使只有一小部分(例如资源或单个方法)发生了变化也是如此,因此这可能会成为一个主要瓶颈。
编译为原生代码。Android L 引入了 ART,这是一种新的 Android 运行时,它会预先编译应用,而不是像 Dalvik 那样即时编译应用。这样可以加快应用的运行速度,但安装时间会延长。这对用户来说是一个不错的权衡,因为用户通常会安装一次应用,然后多次使用它,但会导致开发速度变慢,因为应用会被安装多次,而每个版本最多只会运行几次。
bazel mobile-install
的方法
bazel mobile-install
做出了以下改进:
分片 dexing。构建应用的 Java 代码后,Bazel 会将类文件分片为大致等大小的部分,并对这些部分单独调用
dx
。系统不会对自上次构建以来未发生更改的分片调用dx
。增量文件传输。Android 资源、.dex 文件和原生库会从主 .apk 中移除,并存储在单独的 mobile-install 目录下。这样一来,您就可以独立更新代码和 Android 资源,而无需重新安装整个应用。因此,传输文件所需的时间更短,并且只有发生更改的 .dex 文件会在设备上重新编译。
从 .apk 外部加载应用的部分内容。系统会将一个微小的桩应用放入 .apk 中,该应用会从设备端 mobile-install 目录加载 Android 资源、Java 代码和原生代码,然后将控制权转移给实际应用。除了下文中介绍的少数极端情况外,所有这些对应用都是透明的。
分片 Dexing
分片型 dexing 非常简单:构建 .jar 文件后,工具会将其分片为大致等大小的单独 .jar 文件,然后对自上次 build 以来发生更改的文件调用 dx
。确定要 dex 的哪些分片的逻辑并非特定于 Android:它只是使用 Bazel 的通用更改修剪算法。
第一个版本的分片算法只是按字母顺序对 .class 文件进行排序,然后将列表拆分为大小相等的部分,但事实证明这并不是最理想的:如果添加或移除某个类(即使是嵌套或匿名类),则会导致所有类在其后按字母顺序偏移 1,从而导致重新对这些分片进行 dex 处理。因此,我们决定对 Java 包进行分片,而不是对单个类进行分片。当然,如果添加或移除新软件包,这仍然会导致编制索引的多个分片,但这种情况的频率远低于添加或移除单个类。
分片数量由 BUILD 文件(使用 android_binary.dex_shards
属性)控制。理想情况下,Bazel 会自动确定最佳分片数量,但 Bazel 目前必须先知道一组操作(例如,构建期间要执行的命令),然后才能执行其中任何操作,因此它无法确定最佳分片数量,因为它不知道应用最终会有多少 Java 类。一般来说,分片越多,构建和安装速度越快,但应用启动速度越慢,因为动态链接器必须执行更多工作。理想的范围通常在 10 到 50 个分片之间。
增量文件传输
构建应用后,下一步是安装应用,最好是尽可能少的工作量。安装包括以下步骤:
- 安装 .apk(通常使用
adb install
) - 将 .dex 文件、Android 资源和原生库上传到 mobile-install 目录
第一步没有太多增量:应用要么已安装,要么未安装。Bazel 目前依赖于用户通过 --incremental
命令行选项指明是否应执行此步骤,因为它无法在所有情况下确定是否有必要执行此步骤。
在第二步中,系统会将 build 中的应用文件与设备端清单文件进行比较,该清单文件会列出设备上存在的应用文件及其校验和。系统会将所有新文件上传到设备,更新所有已更改的文件,以及从设备中删除所有已移除的文件。如果清单不存在,系统会认为每个文件都需要上传。
请注意,您可以通过更改设备上的文件(但不能更改清单中的文件校验和)来欺骗增量安装算法。可以通过计算设备上文件的校验和来防范此类问题,但我们认为这样做不值得,因为会增加安装时间。
桩应用
桩应用是从设备端 mobile-install
目录加载 dex、原生代码和 Android 资源的魔法所在地。
实际加载是通过对 BaseDexClassLoader
进行子类化实现的,并且是一项记录相对完善的技术。此操作在加载应用的任何类之前发生,以便将 APK 中的任何应用类放置在设备上的 mobile-install
目录中,以便在没有 adb install
的情况下进行更新。
此操作必须在应用的任何类加载之前发生,这样 .apk 中就不需要应用类,这意味着,更改这些类时需要完全重新安装。
为此,您可以将 AndroidManifest.xml
中指定的 Application
类替换为桩应用。它会在应用启动时接管控制权,并在最早的时间(其构造函数)使用 Java 反射对 Android 框架的内部进行适当调整类加载器和资源管理器。
桩应用的另一项功能是将通过 mobile-install 安装的原生库复制到其他位置。这是必要的,因为动态链接器需要在文件上设置 X
位,而非 root adb
无法访问的任何位置都无法执行此操作。
完成所有这些操作后,存根应用会实例化实际 Application
类,并将对自身的所有引用更改为 Android 框架中的实际应用。
结果
性能
一般来说,在进行细微更改后,bazel mobile-install
可将大型应用的构建和安装速度提高 4 到 10 倍。
我们针对一些 Google 产品计算了以下数据:
当然,这取决于更改的性质:更改基准库后,重新编译需要更多时间。
限制
桩应用使用的技巧并不适用于所有情况。 以下情况会突出显示它在哪些情况下无法按预期运行:
当
Context
转换为ContentProvider#onCreate()
中的Application
类时。此方法会在应用启动期间调用,在我们有机会替换Application
类的实例之前,因此ContentProvider
仍会引用桩应用,而不是真实应用。可以说,这不是一个 bug,因为您不应以这种方式向下转换Context
,但 Google 的某些应用似乎会出现这种情况。由
bazel mobile-install
安装的资源只能在应用内使用。如果其他应用通过PackageManager#getApplicationResources()
访问资源,这些资源将来自上次非增量安装。未运行 ART 的设备。虽然桩应用在 Froyo 及更高版本中运行良好,但 Dalvik 存在一个 bug,会在某些情况下(例如,以特定方式使用 Java 注解时)导致其认为应用不正确,如果应用的代码分布在多个 .dex 文件中,就会出现这种情况。只要您的应用不会触发这些 bug,就应该也适用于 Dalvik(不过请注意,我们并不完全专注于支持旧版 Android)