阿里零售通 App 工程提效实践:提升 50% 的编译速度

前言

当前,大多数 Android 工程都是基于 Gradle 工具进行构建和编译的,一开始,当你的工程不够复杂,或者还只是小型项目的情况下,基本都不需要去关心构建优化的事情,而随着业务变得复杂、代码量的增多以及越来越多的依赖,原有的 单 module 工程变成了多 module 工程,构建时间变得也越来越多。

说到这里,有的同学可能会有疑惑,对于大项目来说,这么多模块和依赖,本来就需要更多的编译时间,还怎么减少构建时间?恰恰相反,实际上越大的项目越能省出来时间。

为了让开发者引起对构建分析的重视,Gradle 官方在最近的版本更新中推出了一个神器 build scan,可视化的深入分析和诊断所有构建相关的数据,并基于此分析结果帮助开发者找出构建问题以及针对构建性能进行优化。

背景

零售通买家端 App 是面向线下小店的一个补货工具,可以快速帮助小店老板通过手机完成进货等功能。随着业务的不断发展,我们的工程规模和代码量也得到了极大的发展,目前我们的客户端工程里的 module 数量达到了40多个,而涉及到的相关依赖库则有 200 多个,已然成为一个中大型项目,可以想象每次编译的时候,我和我的小伙伴们是多么痛苦。

问题

  • 编译速度慢到怀疑人生,一般,我们的项目正常编译时间大概在每次 3 分多钟,如果中间连续编译好几次,时间也会相应成倍增加,同时,电脑 cpu 运转的声音也会越来越响,会有种错觉是在小霸王学习机上进行开发,印象里最深的有一次编译花了十几分钟时间。当然,后面变聪明了,只要听到小霸王学习机的声音,就意味着可以重启电脑了。

    笔者电脑配置:公司标配 MacBook Pro,16G 内存,i7 处理器

  • 莫名其妙的编译问题,无法通过 IDE 的 Run按钮运行,必须要执行命令行编译 ./gradlew clean assembleDebug,本来时间就很慢了,加上 clean 命令后,更是雪上加霜。

  • module 找不到间接依赖库的类,中间有一次升级 Android Studio 版本后,工程里的 module 里的间接依赖库里的类都找不到了,直接飙红了。不过,并不影响正常编译和运行。原因是 *.iml 文件里的没有自动生成间接依赖的 library 导致的。应该是 Android Studio 和 Gradle 低版本兼容问题导致,后面升级到最新版本后解决。

以上编译相关的问题,几乎每天都在消耗着我和我的小伙伴的宝贵时间,也严重影响了日常的开发效率,所以,当看到多个小伙伴在长时间编译后受不了而投来的幽幽眼神后,我想,是时候开始解决这些问题了。

解决

1. 升级 Gradle 版本

在开始任何分析优化工作前,第一件事情是升级你的 Gradle 版本,这也是最简单见效的方法,新的版本,通常意味更好的性能和特性,这也是一条来自 Gradle 官方的优化建议

A guide on performance tuning would normally start with profiling and something about premature optimisation being the root of all evil. Profiling is definitely important and the guide discusses it later, but there are some things you can do that will impact all your builds for the better at the flick of a switch. Use latest Gradle and JVM versions, The Gradle team works continuously on improving the performance of different aspects of Gradle builds.

但是,对于我们的工程来说,升级 Gradle 版本并不是一件轻松的事情,我们有专门的内部打包平台,并引入了它的打包插件,而内部打包插件强依赖了低版本的 Gradle 。虽然,内部打包插件也提供了基于新版本的插件,但是,尝试升级后产生了一些插件兼容问题,而且,后续我们也无法跟随官方的 Gradle 版本进行升级,考虑到通常只有在发版本或者集成测试的时候才会使用内部平台打包。于是,我们采用了另一种方式,通过脚本的控制,让本地日常开发和编译使用新版本的 Gradle,内部平台打包依然走老版本的 Gradle 打包

在升级 Gradle 和 Android Gradle Plugin 版本后,所带来的编译时间的提升非常明显。所以,如果你使用的 Gradle 版本越低,那么升级新版本后,所带来的提升也越明显。

2. 优化和减少 module

我们回顾下 Gradle 的构建生命周期:

  • 初始化阶段: 在初始化阶段,支持单个或者多项目构建,它决定哪些项目模块要参与构建,并为每个项目模块创建一个工程实例。
  • 配置阶段: 在这一阶段,通常是配置每个项目模块。 并执行所有项目模块的构建中的一部分脚本。
  • 任务执行阶段: 在初始化和配置阶段执行完成后,Gradle 开始执行每个参与的任务。

Gradle 提供了一个 --profile 命令,来帮助我们了解在每个阶段所花费的时间,并生成一个报告。比如,你可以执行以下命令来获取到一份报告,位于 rootProject/build/report/profile/***.html

    ./gradlew assembleDebug --profile
复制代码

上图是在我们项目执行的命令后生成的报告,总构建时间花了 3分多种。

  • Summary:构建时间概要
  • Configuration:配置阶段花费的时间
  • Dependency Resolution:依赖解析花费的时间
  • Task Execution:每个任务执行的时间,也是耗时最多的阶段

Tips:Summary 概要里的 Task Execution 时间是每个模块累计相加,实际上多模块的任务是并行执行的。

Task Execution 里是每个模块编译所花费的具体时间。同时,也可以看出编译一个 module 的成本还是比较大的,因为有很多 task 需要执行。所以,接下来的工作就是减少和优化这些的模块。

在重新梳理了下我们的项目模块后,发现有一部分 module 里面其实就只有两三个类,完全没有必要单独 module,可以转移到 app 或者 common-business 模块里,而有一些 module 里的核心逻辑已经抽成了独立 aar 引用,针对残留的代码逻辑,则进行了优化。在完成这些工作后,我们的工程模块数量由 40 多个减少到了 20 多个,是的,我干掉了将近一半的 module。

建议:能不建 module,就不要新建 module,如果确实需要,可以使用 aar 方式代替,aar 编译是有缓存的话,理论上应该比 module 要快的,具体我没有对比过。有兴趣的同学,可以实际对比下试试

3. 一些配置优化

  • 增加 snapshot 缓存策略开关,有时候,为了 snapshot 版本的变动可以实时生效,会加上配置 cacheChangingModulesFor 0, 'seconds',但是,这样就会在每次编译都要去云端比对是否有变动。所以,你可以通过在 local.properties 增加开关来控制,在不需要的时候,关闭它。

      configurations.all {
          resolutionStrategy {
              if (rootProject.ext.cacheChangingModulesForDisable == false) {
                  cacheChangingModulesFor 0, 'seconds'
                  //针对  dynamic (例如2.+)的配置同理
                  //cacheDynamicVersionsFor 0, 'seconds'
              }
          }
      }
    复制代码
  • 避免编译不必要的资源,比如不必要的语言本地化

       android {
         ...
         productFlavors {
           dev {
             ...
             // 只编译以下语言和分辨率的资源
             resConfigs "zh", 'zh-rCN', "xxhdpi"
           }
           ...
         }
       }
    复制代码
  • 不同的 Gradle 版本,一些配置优化也会有区别,更多配置优化可以参考 Gradle 提速:每天为你省下一杯喝咖啡的时间

4. 其他优化

  • 脚本逻辑优化,如避免使用一些网络请求或者 IO 操作。
  • 去除重复和不必要的依赖

结果

好了,经过以上种种努力,终于到了收获的时刻了。因为 Gradle 每次编译的时间误差还是比较大的,为了尽可能保证比对结果的客观性,我们使用以下命令分别在优化前和优化后进行了 3 次编译:

    gradlew --profile --recompile-scripts --offline --rerun-tasks assembleFlavorDebug
复制代码
  • --recompile-scripts: 绕过缓存,强制重新编译脚本
  • --rerun-tasks: 强制重新运行所有 task,忽略任何优化
  • offline: 离线模式编译
构建次数 优化前 优化后 减少率
第一次 3分20秒 1分59秒 - 40%
第二次 2分50秒 1分7秒 - 60%
第三次 2分40秒 54秒 - 67%

以上数据可以看到,在优化后的工程里,编译次数越多,编译时间越少,实际所提升的速度也远超过 50%。

而且,升级版本后,终于可以通过 Run 按钮编译了,最快的一次增量编译时间只有 9s !

总结

虽然我们的编译时间减少到了 1 分多钟,但仍然有较大优化空间,比如为了兼容老的 Gradle 版本,目前我们还没有办法使用 implementation 替换 compile,这样,就可以有效的减少编译时的依赖项,另外,后续也会借助于 build scan 去更深入分析和了解我们的构建信息。

如果你所在的项目,构建编译时间成了一种负担,那么,很有必要引起你的重视。也许,每个项目的实际情况可能都不一样,但是,希望本文可以为你提供一些思路,我相信,随着你的深入分析之后,一定会有更好的办法去解决,另外,重要的是,很少有这样的性能优化,可以在短时间内带来实际的提升效果,所以,现在,立刻开始你的构建优化吧。

如果,对构建优化有什么问题或者不了解的地方,欢迎留言讨论。

参考

Gradle 提速:每天为你省下一杯喝咖啡的时间

最后

一条简短的广告

零售通是阿里巴巴五新战略之一,也是探索新零售模式的一只创业大军,在业务快速发展的同时,我们所面临的技术挑战也越来越高!服务每家店,只为每个家是我们的使命,为了更好的服务我们的使命,我们需要大量的客户端(Android & iOS)、前端、后端同学,期待您和我们一起战斗!

简历请投:[email protected]

双十一来临之际,奉上团队风采照一张

PS:本广告长期有效

猜你喜欢

转载自juejin.im/post/5be3daf3e51d457844614d0b