针对 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会自动启动应用
简介
开发者工具链最重要的属性之一是速度:在更改代码后,如果能在 1 秒内看到代码运行,与必须等待几分钟甚至几小时才能获得有关更改是否达到预期效果的反馈相比,体验会大相径庭。
遗憾的是,用于构建 .apk 的传统 Android 工具链包含许多单体式顺序步骤,并且必须按顺序执行所有这些步骤才能构建 Android 应用。在 Google,对于 Google 地图等大型项目,等待 5 分钟来构建单行更改并不罕见。
bazel mobile-install 通过结合使用更改剪枝、工作分片和对 Android 内部结构的巧妙操作,大幅加快了针对 Android 的迭代开发,而无需更改应用的任何代码。
传统应用安装存在的问题
构建 Android 应用存在一些问题,包括:
Dexing。默认情况下,系统会在构建过程中准确调用“dx”一次,并且它不知道如何重复使用之前构建中的工作:它会再次对每个方法进行 dexing,即使只更改了一个方法也是如此。
将数据上传到设备。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 文件,然后对自上次构建以来发生更改的文件调用
dx。确定要 dex 的分片的逻辑并非特定于 Android:它只是使用 Bazel 的通用更改剪枝算法。
分片算法的第一个版本只是按字母顺序对 .class 文件进行排序,然后将列表剪切为大小相等的部分,但事实证明这种方法并非最佳方法:如果添加或移除了类(即使是嵌套类或匿名类),也会导致其后按字母顺序排列的所有类都移动一个位置,从而导致再次对这些分片进行 dexing。因此,我们决定对 Java 软件包进行分片,而不是对单个类进行分片。当然,如果添加或移除了新软件包,这仍然会导致对许多分片进行 dexing,但这种情况比添加或移除单个类的情况要少得多。
分片的数量由 BUILD 文件控制(使用 android_binary.dex_shards 属性)。在理想情况下,Bazel 会自动确定最佳分片数量,但 Bazel 目前必须先知道一组操作(例如在构建期间要执行的命令),然后才能执行任何操作,因此它无法确定最佳分片数量,因为它不知道应用中最终会有多少 Java 类。一般来说,分片越多,构建和安装速度就越快,但应用启动速度就越慢,因为动态链接器必须执行更多工作。最佳分片数量通常在 10 到 50 之间。
增量文件传输
构建应用后,下一步是安装应用,最好尽可能减少工作量。安装包括以下步骤:
- 安装 .apk(通常使用
adb install) - 将 .dex 文件、Android 资源和原生库上传到 mobile-install 目录
第一步的增量性不大:应用要么安装,要么不安装。Bazel 目前依赖用户通过 --incremental 命令行选项来指示是否应执行此步骤,因为它无法在所有情况下确定是否有必要执行此步骤。
在第二步中,系统会将构建中的应用文件与设备上的清单文件进行比较,该清单文件列出了设备上的应用文件及其校验和。任何新文件都会上传到设备,任何已更改的文件都会更新,任何已移除的文件都会从设备中删除。如果清单不存在,则假定每个文件都需要上传。
请注意,您可以通过更改设备上的文件(但不会更改清单中的校验和)来欺骗增量安装算法。可以通过计算设备上文件的校验和来防止这种情况发生,但我们认为这样做不值得增加安装时间。
桩应用
桩应用是用于从设备上的 mobile-install 目录加载 dex、原生代码和 Android 资源的魔法发生的地方。
实际加载是通过对 BaseDexClassLoader 进行子类化来实现的,这是一种有充分文档记录的技术。这会在加载应用的任何类之前发生,以便 apk 中的任何应用类都可以放置在设备上的 mobile-install 目录中,从而可以在不使用 adb install 的情况下更新这些类。
这需要在加载应用的任何类之前发生,这样就不需要在 .apk 中包含任何应用类,这意味着对这些类的更改需要完全重新安装。
这是通过将 Application 类替换为
AndroidManifest.xml中指定的
桩应用来实现的。当应用启动时,桩应用会接管控制权,并使用 Java 反射在 Android 框架的内部结构上尽早(在其构造函数中)适当地调整类加载器和资源管理器。
桩应用执行的另一项操作是将 mobile-install 安装的原生库复制到另一个位置。这是必要的,因为动态链接器需要对文件设置 X 位,而对于非 root 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)