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,等待 5 分钟构建单行更改在 Google 地图等大型项目中并不罕见。

bazel mobile-install 结合使用更改剪枝、工作分片和对 Android 内部构件的巧妙处理,大大加快了 Android 的迭代开发速度,而且完全不需要更改应用的任何代码。

与传统应用安装相关的问题

构建 Android 应用时会遇到一些问题,包括:

  • Dexing。默认情况下,“dx”在构建中仅被调用一次,并且不知道如何重复使用以前构建中的工作:即使只更改了一个方法,它也会再次对每个方法进行 dex 处理。

  • 将数据上传到设备。adb 并不使用 USB 2.0 连接的全部带宽,上传较大的应用可能需要花费很长时间。即使只有一小部分内容(例如某个资源或单个方法)发生了更改,系统也会上传整个应用,因此这可能是一个主要瓶颈。

  • 编译为原生代码。Android L 引入了 ART,这是一种新的 Android 运行时,它可以预先编译应用,而不是像 Dalvik 一样即时编译应用。这样可以显著提高应用运行速度,但代价是安装时间更长。这对用户来说是一个很好的权衡,因为他们通常只会安装一次应用,然后多次使用,但这会导致开发速度变慢,因为在这种情况下,应用会多次安装,而每个版本最多运行几次。

bazel mobile-install 的方法

bazel mobile-install 做出了以下改进:

  • dex 分片。构建应用的 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 中的应用文件与设备上的清单文件进行比较,后者列出了设备上有哪些应用文件及其校验和。系统会将所有新文件上传到设备,更新所有已更改的文件,以及从设备中删除所有已移除的文件。如果清单不存在,系统会认为每个文件都需要上传。

请注意,通过更改设备上的文件(而不是清单中的校验和)可以忽略增量安装算法。我们可以通过计算设备上文件的校验和来防范这种攻击,但这被认为不值得增加安装时间。

桩应用

在存根应用上,系统会执行从设备上的 mobile-install 目录加载 dex 文件、原生代码和 Android 资源的魔法命令。

实际加载是通过对 BaseDexClassLoader 进行子类化来实现的,这是一种有据可查的技术。此操作在加载应用的任何类之前发生,以便将 APK 中的任何应用类放置在设备上的 mobile-install 目录中,以便在没有 adb install 的情况下更新它们。

此操作需要在加载应用的任何类之前发生,这样 .apk 中就不需要包含应用类了,这意味着对这些类进行更改时需要完全重新安装。

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

存根应用的另一项操作是将 mobile-install 安装的原生库复制到其他位置。这是必要的,因为动态链接器需要对文件设置 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,即在某些情况下,如果其代码分布在多个 .dex 文件上(例如,以特定方式使用 Java 注解时),它会认为该应用是错误的。只要您的应用不会处理这些 bug,它应该也能与 Dalvik 搭配使用(但请注意,对旧版 Android 的支持并非我们的重点)