针对 Android 的快速迭代开发
本页介绍了 bazel mobile-install
如何显著加快 Android 的迭代开发速度。并介绍了这种方法的优势与传统应用安装方法所面临的挑战。
摘要
如需非常快速地对 Android 应用进行细微更改,请执行以下操作:
- 找到要安装的应用的
android_binary
规则。 - 通过移除
proguard_specs
属性停用 Proguard。 - 将
multidex
属性设置为native
。 - 将
dex_shards
属性设置为10
。 - 通过 USB 连接运行 ART(而非 Dalvik)的设备,并在其上启用 USB 调试。
- 运行
bazel mobile-install :your_target
。应用启动速度会比平时慢一点。 - 修改代码或 Android 资源。
- 运行
bazel mobile-install --incremental :your_target
。 - 不必等待太久,
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 个分片之间。
增量文件传输
构建应用后,下一步是安装该应用,最好是尽可能省力。安装包括以下步骤:
- 安装 .apk(通常使用
adb install
) - 将 .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 的支持并非我们的重点)