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 中,该 .apk 会从设备上的移动设备安装目录中加载 Android 资源、Java 代码和原生代码,然后将控制权转交给实际应用。这对应用而言都是透明的,但下述几个极端情况除外。

分片 Dexing

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

分片算法的第一版本直接按字母顺序对 .class 文件进行排序,然后将列表拆分为大小相等的部分,但事实证明这是不是最优的:如果添加或移除某个类(即使是嵌套类或匿名类),则会导致其之后的所有类位移 1,从而导致重新对这些分片进行 dex 处理。因此,他们决定对 Java 软件包进行分片,而不是单个类。当然,如果添加或移除了新软件包,这仍会导致对多个分片执行 dex 处理,但这比添加或移除单个类的频率要低得多。

分片数量由 BUILD 文件(使用 android_binary.dex_shards 属性)控制。在理想情况下,Bazel 会自动确定最佳分片数量,但 Bazel 目前必须先知道一组操作(例如,在构建期间执行的命令),然后再执行其中任何一项操作,因此 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 仍会引用桩应用,而不是实际应用。可以说,这不是 bug,因为您不应该像这样对 Context 进行向下降级,但 Google 的几个应用中似乎发生了这种情况。

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

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