2022 年 BazelCon 将于 11 月 16 日至 17 日在纽约和线上举办。
立即报名!

bazel mobile-install

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

针对 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。默认情况下,“dxot”在 build 中仅被调用一次,它不知道如何重用以前的 build 中的工作:它会再次对每个方法进行 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 代码和原生代码,然后将控制权转移给实际应用。这对应用是完全透明的,下面所述的一些极端情况是完全没有问题的。

分片 dexx

分片 dexing 相对简单:构建 .jar 文件后,工具会将其分片为大致相等的单独 .jar 文件,然后针对自上次构建以来发生更改的文件调用 dx。确定哪些 dex 分片并非特定于 Android 的逻辑:它仅使用 Bazel 的常规更改剪枝算法。

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

分片数量由 BUILD 文件(使用 android_binary.dex_shards 属性)控制。理想情况下,Bazel 会自动确定最佳分片数,但 Bazel 目前必须先知道一组操作(例如,构建期间要执行的命令),然后才能确定最佳分片数,因为它不知道分片数是多少。理想位置数量介于 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 反射,尽早对类加载器和资源管理器进行适当的调整。

桩应用进行的另一操作是将移动设备安装的原生库复制到另一个位置。必须执行此操作,因为动态链接器需要为文件设置 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 存在一个错误,认为其代码在特定情况下分布在多个 .dex 文件中都是错误的,例如以特定方式使用 Java 注解时。只要您的应用不勾勒出这些错误,它也应该适用于 Dalvik(不过,请注意,我们主要关注对旧 Android 版本的支持)