bazel 移动安装

面向 Android 的快速迭代开发

本页将介绍 bazel mobile-install 如何大幅加快 Android 的迭代开发。其中介绍了这种方法与传统应用安装方法相比的优点。

摘要

如需快速为 Android 应用安装细微更改,请执行以下操作:

  1. 找到您要安装的应用,找到其 android_binary 规则。
  2. 移除 proguard_specs 属性即可停用 Proguard。
  3. multidex 属性设置为 native
  4. dex_shards 属性设置为 10
  5. 通过 USB 连接运行 ART(不是 Dalvik)的设备,然后在设备上启用 USB 调试。
  6. 运行 bazel mobile-install :your_target。应用启动会比平时慢一点。
  7. 修改代码或 Android 资源。
  8. 运行 bazel mobile-install --incremental :your_target
  9. 享受不用花费太多的麻烦。

Bazel 的一些命令行选项可能很有用:

  • --adb 告知 Bazel 要使用哪个 adb 二进制文件
  • --adb_arg 可用于向 adb 的命令行添加额外的参数。此选项的一个实用应用是,当您有多个设备连接到工作站时,选择要安装到哪个设备:bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target
  • --start_app会自动启动应用

如有疑问,请查看示例与我们联系

简介

速度是开发者工具链最重要的属性之一:更改代码后,只需等待几分钟(有时数小时)即可看到更改代码是否按预期运行,这二者之间存在很大的差异。

遗憾的是,用于构建 .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 中移除,并存储在单独的移动版安装目录中。这样一来,您无需重新安装整个应用即可独立更新代码和 Android 资源。因此,传输文件所需的时间更少,并且只有已更改的 .dex 文件才会在设备上重新编译。

  • 从 .apk 外部加载应用的某些部分。将一个小型的桩应用放入 .apk 中,该应用会从设备上的移动安装目录中加载 Android 资源、Java 代码和原生代码,然后将控制权移交给实际应用。除了下面所述的少数几种极端情况外,这对应用来说都是透明的。

分片 Dexing

分片 dex 处理相对简单一些:构建 .jar 文件后,工具会将它们拆分为大小大致相同的单独的 .jar 文件,然后对自上一次构建以来发生更改的文件调用 dx。决定要将哪些分片用于 dex 的逻辑并非特定于 Android:它只使用 Bazel 的常规更改剪枝算法。

第一版分片算法只是按字母顺序对 .class 文件进行排序,然后将列表分成相等的部分,但事实证明,这并不是最理想的:如果添加或移除某个类(即使是嵌套类或匿名类),将导致其后面的所有类按字母顺序偏移 1,从而导致再次对这些分片进行 dex 处理。因此,他们决定对 Java 软件包进行分片,而不是单个类。当然,如果添加或移除新软件包,这仍会导致对多个分片进行 dex 处理,但是相较于添加或移除单个类的频率要低得多。

分片数量由 BUILD 文件(使用 android_binary.dex_shards 属性)控制。在理想情况下,Bazel 会自动确定最佳分片数量,但 Bazel 目前必须先知道一组操作(例如,在构建过程中执行的命令),然后才能执行任何操作。因此,它无法确定应用中最终会有多少个 Java 类。一般来说,应用的启动速度越慢,安装进度越慢,应用的启动速度就越慢。最佳平衡点通常介于 10 到 50 个碎片之间。

增量文件传输

构建应用后,下一步是安装它,最好尽可能少费力。安装包括以下步骤:

  1. 安装 .apk(通常使用 adb install
  2. 将 .dex 文件、Android 资源和原生库上传到 mobile-install 目录

第一个步骤没有太多增量:要么应用已安装,要么应用未安装。目前,Bazel 依靠用户来指明是否应通过 --incremental 命令行选项执行此步骤,因为它无法在所有情况下都确定是否有必要执行此步骤。

第二步,将 build 中的应用文件与设备端清单文件进行比较,其中会列出设备上的哪些应用文件及其校验和。系统会将所有新文件上传到设备,更新所有已更改的文件,并从设备中删除所有已移除的文件。如果清单不存在,系统会假定需要上传每个文件。

请注意,通过更改设备上的文件,而不更改清单中的校验和,可以欺骗增量安装算法。可以通过计算设备上文件的校验和来避免这种情况,但这种做法被认为不值得增加安装时间。

Stub 应用

桩应用从设备端 mobile-install 目录加载 dex、原生代码和 Android 资源。

实际加载是通过创建 BaseDexClassLoader 的子类实现的,是一种记录合理的技术。这发生在应用的任何类加载之前,因此 APK 中的任何应用类都可以放在设备上的 mobile-install 目录中,以便在没有 adb install 的情况下进行更新。

此操作需要在应用的任何类之前完成,这样就无需将任何应用类放入 .apk 中,这意味着对这些类所做的更改需要完全重新安装。

AndroidManifest.xml 中指定的 Application 类替换为桩应用。这可控制应用的启动时间,并通过对 Android 框架内部的 Java 反射尽早适当调整类加载器和资源管理器(其构造函数)。

桩应用执行的另一项操作是将移动安装功能安装的原生库复制到其他位置。这是必要的,因为动态链接器需要在文件中设置 X 位,而对于非根 adb 可访问的任何位置,则无法执行此操作。

完成上述所有操作后,桩应用会实例化实际 Application 类,将所有对自身的引用更改为 Android 框架中的实际应用。

成果

性能

一般来说,使用 bazel mobile-install 进行细微的更改后,构建和安装大型应用的速度就能提升到 4 倍到 10 倍。

以下是针对部分 Google 产品计算得出的数据:

当然,这取决于更改的性质:更改基础库后进行重新编译需要更多时间。

限制

桩应用运用的技巧并非在所有情况下都适用。以下情况突出显示了它无法正常运行的地方:

  • Context 转换为 ContentProvider#onCreate() 中的 Application 类时。此方法会在应用启动期间调用,在此之前我们尚未来得及替换 Application 类的实例,因此 ContentProvider 仍会引用桩应用,而不是实际应用。可以说,这不是错误,因为您不应该像这样对 Context 进行向下转换,但这种情况似乎发生在 Google 的一些应用中。

  • bazel mobile-install 安装的资源只能从应用内获得。如果其他应用通过 PackageManager#getApplicationResources() 访问资源,这些资源将来自上次非增量安装。

  • 未运行 ART 的设备。虽然桩应用能够在 Froyo 及更高版本上正常运行,但 Dalvik 存在一个 bug,导致 Dalvik 会在特定情况下(例如以特定方式使用 Java 注解时)将其代码分发到多个 .dex 文件中时,认为应用不正确。只要您的应用不会检测这些 bug,它应该也可以与 Dalvik 搭配使用(但请注意,对旧版 Android 的支持并不是我们的重点)。