分享篇 - 58同城基于Android APP Bundle开发的全新编译模式(编译速度提升70%)
目录
1. Wafers 项目背景
2. 效果展示
3. 实现方案
4. 改造期间遇到的问题
5. 如何接入使用
6. 对比 Instant Run 和 Apply Changes
7. 总结
1. Wafers项目背景
Wafers [ˈweɪfəz] 威化饼。以甜品命名更贴近生活,贴近 Google 的命名风格。同时威化饼象征着一种分层设计,可拆装的动态化设计理念。
随着 58 App 融合的功能越来越多,包体积越来越大,带来如下问题:
- 开发期间的编译速度比较慢;
- 厂商限制内置包的大小,厂商基础包的改造成本越来越大;
- 推广转换率下降。
Wafers 项目正是在这个大背景下产生的,整个项目分为 2 个里程碑:
里程碑 |
进度 |
基于 Android App Bundle (AAB) 改造所有业务线 lib 库,提升开发期的编译速度 |
已完成 |
基于一期改造成果,实现业务模块动态下载运行 |
进行中 |
目前一期已顺利完成,产出了一种全新的开发模式。这种开发模式是业内首创,除了大幅提升了开发期的编译速度外,还具有以下优点:
- 无侵入性。通过开发期增加工程壳,使用自研插件修改字节码适配 AAB (即使用 Wafers 还有另一个使用方向:无侵入地将旧有工程改造成 AAB 模式);
- 改造成本低。只在开发期间生效,不影响线上包。同时保留原有的开发模式,支持两种开发模式的*切换;
- 迎接大势。目前 Google Play 已全面支持 AAB 格式,国内市场未来两年也将逐步迎上。
为此我们将其开源,贡献给更多的业务线,更多的团队!
2. 效果展示
目前 Wafers 已经应用在58同城 APP 中,我们将主业务,房产,黄页,拼车,招聘,二手,金融这几个业务线改造成 AAB 开发模式。同时,我们保留了之前的开发模式,支持一键切换。
- 首次编译:第一次编译,AAB 模式下会构建 base APK 和 dynamic feature APKs。每次编译前执行 gradle clean task,并且释放 Android Studio 内存。
- 增量编译:对开发的 lib 改动一行代码,AAB 模式下只会构建当前业务线的 dynamic feature APK,base APK 无需重新构建。
2.1 整体报告
- 首次编译的速度根据依赖方式的不同,分别有 32% 和 33% 的提升;
- 增量编译的速度根据依赖方式的不同,分别有 67% 和 70% 的提升;
- 库越小速度提升越明显,因为在 debug 开发期间,会向 AAB 工程壳注入插件,修改字节码,以达到无侵入的 AAB 开发模式切换。所以库越小,修改的时间越短;
- 所有业务线都打开 AAB 方式比只打开开发的业务线 AAB 方式更快一些,根据官方文档说明:AAB 采用了并行构建,以前的模式是串行构建一个大的 APK,现在的模式是并行构建多个 APK。
2.2 详细报告
- OS 内存:Macbook pro 16 GB 1600 MHz DDR3 处理器:2.2 GHz Intel Core i7
- IDE Android studio 3.5
- 取样 每个场景执行 3 次,取平均值
开发主业务
开发方式 |
首次编译 |
增量编译 |
对比全 library 依赖提速 |
全 library 依赖 |
3m 20s |
2m 45s |
- |
主业务 AAB 打开,其他业务线 AAB 关闭 |
2m 27s |
1m 8s |
首次编译 26.5%, 增量编译 60% |
主业务 AAB 与其他业务线 AAB 都打开 |
2m 23s |
1m 3s |
首次编译 28.5%, 增量编译 62% |
开发房产业务
开发方式 |
首次编译 |
增量编译 |
对比全 library 依赖提速 |
全 library 依赖 |
3m 20s |
2m 45s |
- |
房产业务 AAB 打开,其他业务线 AAB 关闭 |
2m 16s |
1m 3s |
首次编译 32%, 增量编译 62% |
房产业务 AAB 与其他业务线 AAB 都打开 |
2m 14s |
1m 6s |
首次编译 33%, 增量编译 61% |
开发黄页业务
开发方式 |
首次编译 |
增量编译 |
对比全 library 依赖提速 |
全 library 依赖 |
3m 20s |
2m 45s |
- |
黄页业务 AAB 打开,其他业务线 AAB 关闭 |
2m 9s |
43s |
首次编译 35%, 增量编译 73% |
黄页业务 AAB 与其他业务线 AAB 都打开 |
1m 48s |
42s |
首次编译 46%, 增量编译 73.5% |
开发拼车业务
开发方式 |
首次编译 |
增量编译 |
对比全 library 依赖提速 |
全 library 依赖 |
3m 20s |
2m 45s |
- |
拼车业务 AAB 打开,其他业务线 AAB 关闭 |
2m 15s |
48s |
首次编译 32.5%, 增量编译 70% |
拼车业务 AAB 与其他业务线 AAB 都打开 |
2m 20s |
43s |
首次编译 30%, 增量编译 73% |
开发招聘业务
开发方式 |
首次编译 |
增量编译 |
对比全 library 依赖提速 |
全 library 依赖 |
3m 20s |
2m 45s |
- |
招聘业务 AAB 打开,其他业务线 AAB 关闭 |
2m 14s |
59s |
首次编译 33%, 增量编译 64% |
招聘业务 AAB 与其他业务线 AAB 都打开 |
2m 20s |
47s |
首次编译 30%, 增量编译 71% |
开发二手业务
开发方式 |
首次编译 |
增量编译 |
对比全 library 依赖提速 |
全 library 依赖 |
3m 20s |
2m 45s |
- |
二手业务 AAB 打开,其他业务线 AAB 关闭 |
2m 15s |
46s |
首次编译 32.5%, 增量编译 72% |
二手业务 AAB 与其他业务线 AAB 都打开 |
2m 25s |
49s |
首次编译 27.5%, 增量编译 70% |
开发金融业务
开发方式 |
首次编译 |
增量编译 |
对比全 library 依赖提速 |
全 library 依赖 |
3m 20s |
2m 45s |
- |
金融业务 AAB 打开,其他业务线 AAB 关闭 |
2m 14s |
48s |
首次编译 33%, 增量编译 70% |
金融业务 AAB 与其他业务线 AAB 都打开 |
2m 10s |
42s |
首次编译 35%, 增量编译 74.5% |
3. 实现方案
3.1 AAB 简介
Android App Bundle 是 Android 新推出的一种官方发布格式 (.aab)。通过使用 Android App Bundle 你可以减少应用的包大小,从而提升安装成功率并减少卸载量。
Google Play 就是基于对 aab 文件处理,将 App Bundle 在资源维度,ABI 维度和 Language 等维度进行了拆分。你只要按需组装你的 APK 然后安装即可。如果你的手机是一个 x86,xhdpi 的手机,你在 Google Play 应用市场下载 APK 时,Google Play 会获取手机的信息,然后根据 App Bundle 会帮你拼装好一个 APK,这个 APK 的资源只有 xhdpi 的,so 库只有 x86,其他无关的都会剔除,从而减少了 APK 的大小。
3.2 我们的奇思妙想
看完 AAB 的简介,丝毫没有看到提升编译速度的点,不过 Google 在 AAB 的特点中提到了一点:
缩短构建时间:构建系统(例如使用 Gradle 的 Android Studio 构建系统)针对组织为模块的项目进行了优化。 例如,如果您在具有多核处理器的工作站上启用 Gradle 的并行项目执行优化,则构建系统能够并行构建多个模块,并显着减少构建时间。项目的模块化程度越高,构建性能的提高就越显着。
这一点从上面的数据报告中也可以得到结论,我们将所有业务线都改成了 AAB 模式后,即使是全量构建(所有的 APK 都需要重新构建一遍),编译速度也提升了 30%,这就是得益于 AAB 支持的并行构建特性。但是这一点,还不足以令我们兴奋。AAB 可以构建出一个 base APK 和多个 dynamic feature APK,最后使用 adb install-multiple 命令安装到手机上。
这一点让我们想到了:
58 APP 业务线众多,每个业务线在开发时,大部分需求都是在自己的业务线代码中进行开发。如果我们将所有业务线都改造成AAB 模式,除了可以利用并行构建的优势,还可以在业务线开发时,只构建它自身的 APK。base APK 和其他业务线的 APK 在全量构建后,如果没有修改,后续本地开发无需重新构建。最后直接使用 adb install-multiple 命令一起安装即可,这将大幅减少开发期的编译速度。
3.3 设计方案
有了这个振奋人心的发现,我们小组几个人立马开始设计改造方案,58 APP 的原有的架构图如下:
可以看到,工程结构非常复杂,为此我们定下了几个目标:
- 降低改造影响范围,不能影响全量正式包;
- 降低改造成本,实现无侵入地改造;
- 提高体验,保留之前旧的开发模式,支持快速切换。
经过各种测试,实验,我们最终落地的工程结构图如下:
我们在本地调试期间打开 AAB 开关,对需要被改造的 lib 库加上一个 dynamic feature 工程壳,这层壳用来打出业务 APK,内部的依赖是这个待改造的业务库 aar/源码。这样业务线在没有某个 dynamic feature 库的代码时,也可以通过它们的工程壳打出对应的 APK 文件。
3.4 打包流程
本地调试期间,当 dynamic feature 工程壳编译时会注入自研插件,修改字节码文件,实现无侵入地修改。而 release 全量包则会关闭 AAB 开关,工程结构会和改造之前一模一样,从而不影响全量正式包。
Debug 打包:
Release 打包:
4. 改造期间遇到的问题
虽然最终的方案很完美,但是在改造期间还是遇到了很多问题,这边简单地描述一下:
4.1 关于 AAB 的拆分规则
官方并没有给出 dynamic feature 的拆分规则,为此我们做了一个测试,有如下几个场景:
场景1:
lib1 是否会添加到 dynamic feature APK A 中?
答案:lib1 会添加到 dynamic feature APK 中。
场景2:
构建出 dynamic feature APK A 与 dynamic feature APK B 的时候 lib1 的代码会在哪一个库中呢?都存在? 还是在 base APK 中?
答案:Dynamic feature APK 不能打包相同的 library 依赖,需要将改 library 放到 base APK 中。
场景3:
base APK 中包含 app lib1 与 common 库,dynamic feature APK 只包含自己的库的代码和资源。
规则总结:
- 与 app 库有直接或间接依赖的都会打包到 base APK 中;
- Dynamic feature moudle 直接或间接依赖的库而不被 app 依赖的库会打到 dynamic feature APK 中;
- Dynamic feature moudle 间不能同时依赖相同的库,需要将其放到 base APK 中;
- 只有 dynamic feature moudle 可以依赖 app 库。
以上几点结论,为我们开发设计 Wafers 一期,奠定了重要的基础。
4.2 Dynamic feature R 文件规则
我们开发的插件,解决的主要问题就是 R 文件。之前旧的开发模式,最终所有的库会打包到一个 APK 文件中,所以很多资源访问的问题不会存在,或者说暴露出来,包括开发者引用错误的 ID。
首先我们调研了下 dynamic feature R 文件的引用规则:
- Dynamic feature moudle 如果引用 base moudle 的 R 文件会自动替换为数值;
- 不是 dynamic feature 而被打入 dynamic feature APK 中的库,依然是 pkgName.R.id 的形式存在,不会被替换成数值。
改造过程中遇到了如下几个 R 相关的问题:
- (1) Dynamic feature APK 中引用 base APK 的资源
解决方案:增加一个 dependences 中间库用作 R 资源收敛,使用插件排查注入包名前缀。使用 Wafers 开发模式,都需要新增一个用作 R 资源收拢的中间库。
- (2) Dynamic feature APK 中反射获取资源
解决方案:插件中拦截反射资源函数,使用代理 Resources 方式替换:优先从自身库查找 (pkg 需要加上 .featureName),找不到再从 base APK 中查找。
- (3) Dynamic feature APK 中和 base APK 中有同名 id,但编码时错误引用了 base APK 中的 id 值
解决方案:插件拦截 findViewById 函数,替换成自定义实现的 findViewById:针对 Activity、View、Dialog、Window 分别从当前库,base APK 查找,最后使用反射方式处理。
- (4) Base APK 中使用了 id 查找,但其引用的 layout 被 dynamic feature 替换了
解决方案:通过插件修改 base APK,加白名单列表,修改指定的类。
4.3 国产 VIVO, OPPO 不支持 adb install-multiple
adb install-multiple 在国产的 VIVO, OPPO 手机上会安装失败,接入方可以参考 demo 关闭所有 AAB 开关,在 Wafers 二期我们将把 dynamic feature APK 内置到 assets 解决此问题。
5.如何接入使用
Wafers 目前仅在 58 内部进行了开源,后期会逐步向外界开放。58 的同学如果有兴趣对自身的项目做改造,可以在公司 gitlab 仓库中搜索 Wafers, 里面有详细的接入文档,使用文档和 demo。在改造期间遇到问题,可以在 Wafers gitlab 的 issues 反馈问题,我们会及时进行跟进处理, 同时也欢迎提交 MergeRequest,为Wafers 贡献一份力量。
6. 对比 Instant Run 和 Apply Changes
6.1 Instant Run
Instant Run, 是 Android Studio2.0 以后新增的一个运行机制,能够显著减少第二次及以后的构建和部署时间。简单通俗的解释就是,当你在 Android Studio 中改了你的代码,Instant Run 可以很快的让你看到你修改的效果。而在没有 Instant Run 之前,你的一个小小的修改,都肯能需要几十秒甚至更长的等待才能看到修改后的效果。
Instant Run 主要解决以下两个问题:
- 减少构建和部署 app 到手机的时间;
- 热更新代码改动,无需重启 app 或者 activity。
为了实现这两个目标,Instant Run 通过重写 APK 的构建流程往每个类里面去注入钩子来达到类的热替换。
Instant Run 这个技术是基于 Transfrom-API 技术,但是 Android Studio 3.5 Instant Run 就被废弃了,主要是 Instant Run 诟病太多,对于小型的应用,Instant Run 确实很好用,能够节省构建和部署的时间,并且不会出错。但是对于大型复杂应用,它会导致更长的构建时间,同时由于 Instant Run 构建过程和正常的 app 构建存在冲突,常常出现另开发者抓狂的错误,如:
- 偶尔变更代码没生效;
- 容易卡开发机器;
- 各种莫名其妙的 crash 和 bug.
6.2 Apply Changes
Android Studio 3.5 引入了 Apply Changes,它取代了旧的 Instant Run。Instant Run 是为了更容易地对应用程序进行小的更改并测试它们,但它会产生一些问题。为了解决这一问题,Google 已经彻底删除了 Instant Run,并从根本上构建了 Apply Changes ,不再在构建过程中修改 APK,而是使用运行时工具动态地重新定义类,它应该比立刻运行更可靠和更快。
和 InstantRun 不同的是,它不会在构建过程中去修改 APK。它依赖的是 Android 8.0 开始虚拟机支持的特殊指令 (JVMTI) 来进行类的替换。ART 在 8.0 上才初步实现此接口,9.0上 基本成型。事实上,Android Studio 的 profiler 也依赖这个接口,用来分析应用程序的堆内存/CPU/GC 等关键参数。
当连接上 Android 8.0 及以上的设备时,Android Studio 将会多出三个按钮,Apply Changes 部署资源和代码改动到手机,重启 activity,但是无需重启应用。仅部署代码改动到手机,不需要重启 activity 和应用。
想使用 Apply Changes 有两个条件:
- APK 必须是 debug 包;
- 必须在 Android 8.0 以上的手机上运行。
6.3 对比
抛开 Instant Run, Wafers 在快速构建上和 Apply Changes 的对比:
优点:
- 没有 Android 8.0 的限制;
- 继承了 AAB 自身的优势;
- 复杂的工程结构下,在 58 App 中的体验结果是:Wafers 速度比 Apply Changes 快 20%;
- Apply Changes 在实际体验中,操作按钮经常莫名地会变灰 (不可操作状态),且经常代码不生效,而 Wafers 不存在这种问题。
缺点:
- 不支持 OPPO/VIVO 手机;
- 会重启 app.
快速构建是 Wafers 的一个优点,但不是唯一的优点,Wafers 支持在不侵入原始代码的情况下,将旧有项目改造成 AAB 模式,同时为后面动态安装加载提供基础。
7.总结
Wafers 是种全新的开发模式,在大型,复杂,多模块的项目中,可以有很大的发挥空间。
同时改造成本较低,只要遵循 Google 的 AAB 文档进行改造,再应用 Wafers 的插件,即可实现少量代码快速无侵入的模式切换。但是 AAB 本身有一些限制,如 dynamic feature 库之间不能互相依赖,dynamic feature 的 manifest 中使用的资源需要提前放在 base APK 中 (因为会提前进行 manifest 合并) 等,所以接入改造的项目可以根据自身业务的复杂度选择性地修改某些模块。每个开发模式都有它需要遵循的规则,AAB 是 Google 在模块化,动态化领域更深入,更彻底的一个产物。也是大势所趋,所以朝前走肯定需要顺应一些规则,正如当年的我们从 Eclipse 切换到 Android Studio,从 Ant 切换到 Gradle,从 Java 切换到 Kotlin。
Wafers 2 期,我们将实现动态包的产出,如各种厂商包,业务推广包,这将大大减少包大小。同时可以脱离Google Play 的束缚,在国内的应用市场实现 AAB 模式的动态包下载和装载,也为迎接 AAB 模式在国内应用市场的普及做好准备。
参考文献
https://developer.android.com/platform/technology/app-bundle
https://zhuanlan.zhihu.com/p/86995941
本文地址:https://blog.csdn.net/u014294681/article/details/105967678
上一篇: 长孙无忌为何会拒绝李世民想立吴王李恪为太子的想法?
下一篇: 靠谱的app加固分享(未完成)