网易新闻是一个综合性的媒体,近年来,迭代业务不断加剧,新闻项目的代码也日趋臃肿,而沉重的代码也让项目构建速度如拴上铅块般下滑,所以我们按时间维度,开启了一系列Gradle 构建速度优化的尝试,走上了编译效能的自赎之路。
1
组件化编译
起初,我们考虑利用组件化的特性来间接提升构建性能,带来研发效能的提升。
实现组件化架构可以让项目不同组件之间的边界划分清晰,利于架构和团队向粒度更精准的模式转型,而组件化带来的“组件独立编译”特性,也能将组件从繁重的集成编译中脱离出来,显著优化构建速度。
在使用流行的组件化设计思路,我们将组件化项目划分为了两个实现阶段,在第二阶段将各组件间的依赖确定清晰,以实现独立编译,构建思路如 图1 所示。
图1 组件化设计
但由于项目和人力资源等因素的影响,我们的组件化周期持续较长,而长期的战线迫使我们不得不将目光再次回到 Gradle 优化上。
2
Artifactory
由于网络条件限制等原因,在初次获取第三方仓库依赖时,经常会发生长耗时的场景,这一个场景在个例中发生数次或许可以忍受,但发生在团队中时,就会让影响规模升级,产生整体团队的研发效能的损耗。
所以我们基于 JFrog Artifactory搭建了依赖管理仓库,在获取流行依赖时,Gradle 和 CI 直接通过 JFrog 拉取相关依赖。
在获取 JFrog 之外的依赖时,Gradle 和 CI 将在 JFrog 协作者将依赖第一次获取到后,缓存至 JFrog 缓存仓库,如 图2 所示,在下一个协作者再次获取该依赖时,可直接通过 JFrog 拉取,此操作降低了协作者重复向依赖原仓库获取资源的耗时。
除此之外,JFrog 还具有对不同仓库建立独立存储的能力,令仓库之间的边界更加清晰。
图2 Artifactory 获取依赖流程
3
最小编译模式
3.1 分析工具3.1.1 Build Scan
Gradle Enterprise 提供了 Build Scan 可以很有效的帮助开发者进行构建性能的分析,并找出拖慢项目构建速度的根本原因。除此之外,Build Scan 也为实现构建性能监控提供了便利,通过命令行执行 Build Scan,在构建分析结束后会自动将报告上传到 scans.gradle.com,生成的报告样例如 图3 所示。
图3 Build Scan 报告样例
我们将项目构建过程中耗时 1s 以上的 Task筛选后进行分析,发现占比比较大的 Task 仍然是Transform 和 Compile过程,占比分析如 图4 所示。
图4 Task 耗时占比分析
究其原因,主要影响点在于代码规模上过于庞大,导致每次 Transform 和 Compile 过程冗长,而最小编译模式可以解决这样的历史性问题。
3.1.2 ClassyShark
利用 Google 开源的 ClassyShark 可以帮助我们分析 APK 中的文件占比情况,如 图5 所示,ClassyShark 可以根据包名统计出其 class 在包中的占比,而占比较大的部分由是非必要参与构建过程的 class,如渠道通知相关的 SDK,就可以在最小编译模式中被忽略。
图5 ClassyShark 分析样例
3.2 SDK/源码隔离
通过在 gradle.properties 添加最小编译模式 MiniDependencies 的标记位,在 build.gradle 中获取标记位信息,可以在sourceSets中控制是否引用部分源码或在 dependencies 中控制是否依赖部分 SDK。
3.2.1 方案设计
在 gradle.properties 添加最小编译模式 MiniDependencies 的标记位后,在 build.gradle 中可以获取到标记位信息,并赋值给被最小编译的忽略 SDK / 源码 开关,该开关可以设计为总开关,也可针对每个 SDK 单独设计子开关,如 图6 所示。
图6 最小编译开关设计
当未开启最小编译模式时,SDKEnable 将置为 true,build.gradle 中会在 sourceSets.main 闭包中判断 SDKEnable,通过 java.srcDic 建立项目与部分源码目录的关联关系,在 dependencies 闭包中判断 SDKEnable,建立项目与 部分 SDK 的关联关系,如 图7 所示。
当开启最小编译模式时,SDKEnable 将置为 false, sourceSets.main 和 dependencies 闭包中的关联关系也会动态被解除。
图7 最小编译依赖配置
当开启最小编译模式时,项目将不依赖这些可忽略的源码或 SDK,如 图8 所示,这样可以大幅降低项目的 Transform 和 Compile 过程耗时。
图8 最小编译模式构建
而不依赖的 SDK 需要通过代理类将代码中直接依赖的 API 进行一层包装,并抽象出相应的接口协议,由对应的 SDKManager 来控制 SDK 代理类,即接口的实现类的加载,在 SDKManager 初始化时,通过反射查找并实例化接口实现类。
其中,通过以下区域划分的隔离来实现以接口隔离 SDK:
接口的实现类放在 Optional 区域,该部分在开启最小编译模式时由 java.srcDic 控制不参与编译,在关闭最小编译模式时参与编译
接口放在 Host 区域,不论是否开启最小编译模式,均参与编译
在开启最小编译模式时,接口实现类无法被找到,会通过防御式判断而返回,在关闭最小编译模式时可以成功找到并初始化接口实现类,进而调用到 SDK 相关 API,如 图9 所示。
图9 SDK 的接口隔离
3.2.2 忽略类型选择
在最小编译模式中,通常忽略的 SDK /源码 包括以下几种类型:
厂商 PUSH
非核心统计相关
开发中非强依赖的其他功能(如 Patch 相关)
通过 build scan 测试 10 次获取平均值,在开闭最小编译模式对比可得,项目首次构建速度提升20%,Clean后再构建,构建速度提升约25%。
4
参数调优
参数调优是 Gradle 构建速度优化最基础的步骤,在网络博主中已经提供了多套相关成熟的调优方案,而对于团队管理来说,让团队统一配置是需要考虑的一点。
通过 Hook Compile 过程,我们动态插入了一个 Task,来校验并配置 gradle.properties 相关信息,其中包括 Daemon、Build Cache 等相关配置,来达到配置的统一,同时也保留了开发者对于 gradle.properties 中的一些自定义配置,如 图10 所示。
图10 参数调优统一配置
除此之外,对于 Debug 模式下,我们还可选择性的禁用了 Lint 等 Task,以节省一部分 Task 执行耗时,在 Release 模式下,则会忽略掉部分参数调优,以保证编译的稳定性。
5
远程编译
在经过上述构建速度优化后,我们的平均构建性能提升了约 50%,但对于大部分同事来说,CPU 资源有限,而庞大的 Project 编译起来,依然需要庞大的内存开销,且 CPU 的负荷也过重,编译效能还是不能达到理想的预期。所以考虑我们是否可以借鉴 Jenkins 的思路,提供一台性能卓越的远程超级计算机,将编译的任务都转到远程来处理,将编译结果同步到本地,以解放本机 CPU。
5.1 远程编译设计
通过 SSH,在本机编译时将 Project 文件同步到远程机器,在远端执行 Gradle 命令,在生成编译结果后再将编译结果同步到本地,即完成了大致的流程,如 图11 所示。
图11 远程编译设计
5.1.1 文件同步
值得一提的是,在 Local 和 Remote 同步时,利用"Quick Check" 算法,我们可以清晰地对比出 Local 和 Remote 之间的增量文件,只同步这些增量文件,但.git 目录的变更频率高,且体积占 Project 比例较大,在同步时需要忽略 .git 目录的传输,否则会拉慢整体传输效率。
在同步编译结果时,为了保证传输效率,我们也仅同步 APK。
5.1.2 兼容 Android Studio
借助 Android Studio 的Configuration组件,还能够实现一键远程编译并安装 APK 到手机,而自定义的 Configuration 也支持保存在 Project 中,便于在 Team 中进行远程编译的 Configuration版本控制管理和协作开发,如 图12 所示。
同步编译结果时,我们虽然仅同步了 APK,但由于 "Apply Changes" 不依赖于其他编译中间产物,所以这也并不会使 "Apply Changes" 失效。
图12 远程编译的 Configuration 管理
5.2 远程编译流程
整体的远程编译流程大致分为三个阶段:SSH 配置,remote 操作和 local 操作。SSH 配置阶段主要将 Key 和 Config 通过脚本拷贝到 Team 成员的本机中,实现自动配置,remote 操作则涉及远程打包相关,而 local 操作依托于 Android Studio 的 Configuration,同步 APK 到本机并自动安装到手机中,如 图13 所示。
图13 远程编译流程
5.3 影响远程编译性能的因素
分别使用不同 CPU 配置的两种计算机进行同项目的构建,以下表中的开发主机为参照,可见,CPU 等性能的提升能直接带动构建效率的提升。
计算机CPU内存非增量编译性能增量编译性能开发主机
2.6 GHz 双核
Intel Core i5
16 GB 1
1
远程编译主机
3.29 GHz 四核
Intel Core i5
16 GB↑82%↑57%
而将项目以远程编译方式构建时,除首次文件同步有一定时间折损外,增量同步场景下,文件同步的时间可忽略不计。可见,当团队想使用远程编译时,前提条件是需要获得一台备用主机,且该主机的性能至少要优于团队主机性能平均水准,一台远程公共主机预期应能支持 20 人以下的团队参与远程编译。
除此之外,项目规模对构建效率有影响,对远程编译性能具有同样的影响。
6
项目收益
在进行了如上的系列优化后,通过 Build Scan 多次测试数据对比分析,可得到总体数据如下:
在非增量编译时,项目的构建速度总体提升 90%
在增量编译时,项目的构建速度总体提升 75%
由此可见,在满足一定远程计算机性能的条件下,就可以达到令构建速度“势如闪电”。
我们根据每项优化的具体时间占比可以得出 图14 中的数据,数据由于依赖于优化的先后顺序,不能作为衡量优化性能的唯一标准,提供仅供参考。
图14 构建优化分析
- End -
作者简介
李云鹏 2016 年加入网易传媒,目前主要负责架构和性能优化相关工作。QCon 大会、Google GDG 讲师,著有《移动开发架构设计实战》等书籍。
是谁这么优秀,想要点在看
热门跟贴