bazel 移动安装

报告问题 查看源代码 每夜 build · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

快速迭代开发 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 会自动启动应用

如有疑问,请参阅示例与我们联系

简介

开发者工具链最重要的属性之一就是速度:更改代码并在 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 文件进行排序,然后将列表切分为大小相同的部分,但事实证明这并不是最优方案:如果添加或移除某个类(即使是嵌套类或匿名类),都会导致按字母顺序排列在该类后面的所有类向后偏移一个位置,从而导致系统再次对这些分片进行编入索引。因此,我们决定对 Java 包进行分片,而不是对单个类进行分片。当然,如果添加或移除新软件包,这仍然会导致编制索引的多个分片,但这种情况的频率远低于添加或移除单个类。

分片数量由 BUILD 文件(使用 android_binary.dex_shards 属性)控制。理想情况下,Bazel 会自动确定最佳分片数量,但 Bazel 目前必须先知道一组操作(例如,构建期间要执行的命令),然后才能执行其中任何操作,因此它无法确定最佳分片数量,因为它不知道应用最终会有多少 Java 类。一般来说,分片越多,构建和安装速度越快,但应用启动速度越慢,因为动态链接器必须执行更多工作。理想的范围通常在 10 到 50 个分片之间。

增量文件传输

构建应用后,下一步是安装应用,最好能尽可能减少工作量。安装包括以下步骤:

  1. 安装 .apk(通常使用 adb install
  2. 将 .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 产品计算了以下数据:

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

限制

桩应用使用的技巧并不适用于所有情况。 以下示例突出显示了该功能无法正常运行的情况:

  • ContextContentProvider#onCreate() 中被转换为 Application 类时。此方法会在应用启动期间调用,在我们有机会替换 Application 类的实例之前,因此 ContentProvider 仍会引用桩应用,而不是真实应用。可以说,这不是一个 bug,因为您不应以这种方式向下转换 Context,但 Google 的某些应用似乎会出现这种情况。

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

  • 未运行 ART 的设备。虽然桩应用在 Froyo 及更高版本中运行良好,但 Dalvik 存在一个 bug,会在某些情况下(例如,以特定方式使用 Java 注解时)导致其认为应用不正确,如果应用的代码分布在多个 .dex 文件中,就会出现这种情况。只要您的应用不会触发这些 bug,就应该也适用于 Dalvik(不过请注意,我们并不完全专注于支持旧版 Android)