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,在 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
。对于自上次 build 以来未发生更改的分片,不会调用dx
。增量文件传输。Android 资源、.dex 文件和原生库会从主 .apk 中移除,并存储在单独的移动安装目录下。这样一来,您就可以独立更新代码和 Android 资源,而无需重新安装整个应用。因此,传输文件所需的时间更短,并且只有已更改的 .dex 文件会在设备上重新编译。
从 .apk 外部加载应用的部分内容。一个微小的桩应用会被放入 .apk 中,该桩应用会从设备上的移动安装目录加载 Android 资源、Java 代码和原生代码,然后将控制权转移到实际应用。除了下文所述的少数极端情况外,这对应用来说是完全透明的。
分片 Dexing
分块 dexing 相当简单:一旦构建了 .jar 文件,工具就会将它们分块为大小大致相等的单独 .jar 文件,然后对自上次构建以来发生更改的文件调用 dx
。用于确定要 dex 的分片的逻辑并非特定于 Android:它只是使用了 Bazel 的常规更改剪枝算法。
分片算法的第一个版本只是按字母顺序对 .class 文件进行排序,然后将列表分成大小相等的部分,但事实证明这种方法并不理想:如果添加或移除了某个类(即使是嵌套类或匿名类),也会导致其后按字母顺序排列的所有类偏移一个位置,从而导致这些分片再次进行 dex 处理。因此,我们决定对 Java 软件包进行分片,而不是对单个类进行分片。当然,如果添加或移除了新软件包,这仍会导致对许多分片进行 dexing,但这种情况的发生频率远低于添加或移除单个类。
分片数量由 BUILD 文件(使用 android_binary.dex_shards
属性)控制。在理想情况下,Bazel 会自动确定最佳分片数量,但 Bazel 目前必须在执行任何操作之前了解操作集(例如,在 build 期间要执行的命令),因此无法确定最佳分片数量,因为它不知道应用中最终会有多少 Java 类。一般来说,分片越多,build 和安装速度就越快,但应用启动速度会变慢,因为动态链接器必须执行更多工作。最佳分片数通常介于 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
类替换为桩应用。它会在应用启动时接管控制权,并使用 Java 反射在 Android 框架的内部结构上,在最早的时刻(其构造函数)相应地调整类加载器和资源管理器。
桩应用还会将移动安装程序安装的原生库复制到其他位置。这是必需的,因为动态链接器需要为文件设置 X
位,而对于非 root adb
可访问的任何位置,都无法执行此操作。
完成所有这些操作后,桩应用会实例化实际的 Application
类,并将对自身的所有引用更改为 Android 框架内的实际应用。
结果
性能
一般来说,bazel mobile-install
可将大型应用在小幅更改后的构建和安装速度提高 4 到 10 倍。
以下数字是针对部分 Google 产品计算得出的:
当然,这取决于更改的性质:更改基础库后重新编译需要更多时间。
限制
桩应用所用的技巧并非在所有情况下都有效。 以下情况表明该功能无法正常运行:
当
Context
在ContentProvider#onCreate()
中强制转换为Application
类时。此方法在应用启动期间调用,此时我们还没有机会替换Application
类的实例,因此ContentProvider
仍会引用桩应用,而不是实际应用。从理论上讲,这不算 bug,因为您不应以这种方式进行向下转换,但这种情况似乎在 Google 的一些应用中发生。Context
由
bazel mobile-install
安装的资源只能从应用内部访问。如果其他应用通过PackageManager#getApplicationResources()
访问资源,这些资源将来自上次非增量安装。未运行 ART 的设备。虽然桩应用在 Froyo 及更高版本上运行良好,但 Dalvik 存在一个 bug,在某些情况下(例如以特定方式使用 Java 注释时),如果应用的代码分布在多个 .dex 文件中,Dalvik 会认为该应用不正确。只要您的应用不触发这些 bug,它也应该能与 Dalvik 搭配使用(不过请注意,我们并非完全专注于支持旧版 Android)