针对 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 可访问的任何位置,都无法执行此操作。
完成所有这些操作后,桩应用会实例化实际的
actual 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, 它也应该适用于 Dalvik(但请注意,我们并不完全专注于支持旧版 Android)