背景
目前字节 Android 的一些超大型项目均从原来的多仓二进制的研发模式切换到了 Monorepo 全源码,并在研发效能方面取得了较大的收益。关于 Monorepo 全源码模式下的一些技术挑战及解决方案后续会有文章单独阐述。在全源码改造的过程中,作为 build 方向的基建团队,我们也不断克服了很多 Gradle 生态的问题与挑战,对构建系统方面有了更进一步的理解。
提到超大型仓库的 Monorepo,就不得不提到 Bazel 构建系统。Google 内部使用 Bazel 作为超大仓(TB 级别)的构建系统,足以证明 Bazel 构建系统优异的性能表现。目前字节内服务端、iOS 端均采用 Bazel 作为构建系统来演进 Monorepo,业界关于 Bazel 的原理文章也很多,但大部分都围绕在工程改造方面或者在单一阐述 Bazel 的设计。这里就有几个问题:
-
单一的阐述一个构建系统,没有横向的比较是不太客观的,也不太容易理解核心的理念及技术 -
很多人有这个疑问:既然 Bazel 这么出色,为什么在 JVM 领域没有形成大的生态,为什么 Android 不使用 Bazel
本篇文章我们将尽量通俗易懂的阐述下 Bazel 与 Gradle 的设计异同及其在 Android 构建方面的表现,希望能解决这些疑惑。
构建系统核心概念
什么是构建系统
官方回答:“用来从源代码生成用户可以使用的目标(targets)的自动化工具。目标可以包括库、可执行文件、或者生成的脚本”。
叫的上名字的构建系统很多,如 CMake、Maven、Gradle、Bazel 等。
其中 Bazel 和 Gradle 的生态尤为庞大。如果给各自贴个标签,那么:
-
Bazel:应用于 Google 大型项目,倾向于支持多语言,以性能著称。 -
Gradle :JVM 体系最好用的构建系统之一,广泛应用于 Java 和 Android 项目,同样支持多语言。
一个成熟的构建系统的核心要素大概包括以下几个维度:
核心调度机制: 构建系统的“发动机”,调度能力的设计一定程度上决定了构建系统的上限。
构建规则 DSL : 任何构建系统都有一套自己的 DSL 供开发者使用,用来定义要构建规则和目标。Gradle 的 DSL 语言是 Groovy 和 kotlin,特点是更灵活强大,但灵活性也给其生态带来了巨大的副作用;而 Bazel 使用 Starlark 作为 DSL,可以理解为一个阉割版的 python,虽然限制了 DSL 部分能力,反而实现了系统的可控性。这一点整体上来讲 Bazel 显得更有远见一些。
缓存系统: 缓存系统设计的好坏和核心调度机制同等重要,决定了构建系统的上限,一个性能好的构建系统一定在缓存方面有着优雅的设计。二者在缓存方面均下足了功夫,从缓存的角度来看无法评价二者的优劣。
依赖管理系统: 一个复杂的巨型工程可能具有很复杂的依赖关系,因此一个易用灵活高性能的依赖管理系统至关重要。下文中会对二者在这一方面进行对比。
扩展能力:Bazel 和 Gradle 都声称支持多语言,扩展性强,最直观的体现就是插件系统。Gradle 可以通过自定义插件实现任意能力的扩展,比如 Android 构建的过程就是 Google 开发了一套 Android Gradle Plugin 运行在 Gradle 上完成的;Bazel 对应于的体系则被称为 rules,Bazel 也是通过提供 rules_android 来完成 Android 构建。从扩展性的角度来看,二者均非常出色。
rules_android :https://github.com/bazelbuild/rules_android
从上面的几个要素分析看,Bazel 与 Gradle 均具备了大型构建系统的核心要素,接下来我们逐步深入,从更细的维度来进行下对比,在进入之前先来看看两者在构建流程方面的差异,方便大家了解下文中的一些概念。
构建流程
当我们执行一条构建命令时,构建系统基本上都会经历三个阶段来完成构建。
Load 阶段:进行初始化构建系统,加载配置文件,找到入口 Target or Task。针对 Bazel 而言即为加载 WORKSPACE 和 BUILD 文件,找到需要执行的 Target ;针对 Gradle 即为加载 settings.gradle 及 build.gradle 文件,找到目标 Task。
Analysis 阶段:解析脚本及规则进行依赖解析,完成依赖的下载及 Action or Task 的注册,形成待执行的 DAG (有向无环图)。Bazel 的执行单元称为 Action,Gradle 的执行单元称为 Task,为了方便描述,下文统称为 Task。注意这里表述的不够严谨,对于 Bazel 来说,这个 DAG 更复杂一些,为了方便理解,暂且这么认为。
Execution 阶段:按照构建系统的调度策略执行对应的 Task,得到构建结果。这个阶段也是 Bazel 和 Gradle 差异最大的地方。
深度对比
终于来到了本篇文章的核心部分,我们接下来从理念、并发能力、增量机制、配置阶段差异及其他核心能力等五个方面来进行一下阐述。为了避免枯燥,尽量做到通俗易懂。
理念
Gradle 在 2007 年就进行了开源,当初的对标目标是 Maven,众所周知,Maven 服务于 Java 生态,早年的 Gradle 性能也是非常糟糕,这个印象可能至今都没有很好的扭转过来。
Bazel 最早诞生于 Google 内部,为了应对 Google 内部超大型多语言仓库的瓶颈与挑战而开发,于 2015 年开源。
二者的设计理念和自我定位有着较大区别:
理念 | 解释 | |
---|---|---|
Bazel | Artifact-based build system产物驱动型 |
只声明需要什么,依赖什么产物;Bazel 会自动依赖产物来关联相关的 action,这些 action 是否并发执行也仅取决于产物的依赖情况。举个例子:定义 ActionX和 ActionY,ActionX input 中依赖了一个 A.jar ,ActionY 会产出 A.jar 产物,则 ActionX 会自动隐式依赖 output 中含有 A.jar 的 ActionY。 |
Gradle | Task-based build system任务驱动型 |
Gradle 构建系统的视角为 Task,Task 内部可以定义任意的逻辑与能力;比如 TaskX 有个 input 为 A.jar,A.jar 由 TaskY 产生,需要显示的声明 TaskX depends on TaskY。 |
从表格中可以感受到 Bazel 的“产物驱动”的模式自动化程度貌似更高一些。其通过 “产物依赖” 建立 Action 自动隐式依赖
的形式能带来诸多好处:
-
构建系统层面有更多的信息与控制权;在构建系统层面非常容易拿到 A.jar 相关的 Action 信息,比如 A.jar 变了,应该执行什么,不应该执行什么的粒度就可以做的更细。而 task 驱动型相对来讲就会有些吃力。 -
理念的差异其实一定程度也会影响到构建系统的并发能力; 产物驱动型
的构建系统的并发能力一定程度上会更高。
这里理解起来可能还是有点抽象,下面的章节中会不断地在示例中阐述这种里面层面的差异。接下来先来看看并发能力方面的差异。
并发能力
并发能力是衡量一个构建系统的核心指标,我们通过一个最小化的场景来对比一下。
如图所示,假设有三个任务 T1、T2 和 T3。T2 和 T3 从直观感受上并没有依赖关系,一般情况下完全可以并发执行。为什么说是一般情况下呢?如果 T2、T3 操作的了同一个文件,那此时并发就可能出现问题。
对于 Bazel 而言应对很轻松,产物驱动型
的理念和设计能感知到 T2,T3 是否操作了同一个文件,从而决定是否完全并发。
对于 Gradle 而言就比较麻烦了。在 Gradle 任务驱动型
机制下,对文件修改的感知能力不如 Bazel,当出现 T2,T3 都属于同一个 module 时,无法准确判断 T2,T3 是否存在 overlap,因为它的机制下,同一个 module 下的所有 Task 执行期间会持有一种相同的锁来保证正确性。那是不是只能串行呢?其实也不然。Gradle 提供了一种称为 Worker API 的机制来弥补这个缺陷,基本思想为既然无法整体判断,那就进行拆分,保证资源共享的部分依然串行,将耗时的大头部分扔到后台线程池去执行,执行完通知进行资源释放即可,从而间接的实现了此种场景的并发。感兴趣的同学可以看之前我们发表过的 Gradle 调度机制的文章。
Worker API :https://docs.gradle.org/current/userguide/worker_api.html
Gradle 调度机制:组件发布效率提升15倍是怎么做到的——基于Gradle调度机制深度研究与优化
总结
Bazel 的并发性能更好,而 Gradle 也并没有其他 Task 驱动型的构建系统的那么不堪,通过一种不太优雅的机制弥补了这一缺陷,但是这种机制引入了大量的 wait-notifyAll 的唤醒行为。单纯从并发性能上看,Bazel 更加强悍,但二者的差距并没有外界认为的那么大。
大致了解了并发能力的差异后,我们以一个增量编译的场景为契机来了解下二者在 Execution 阶段的差异。
快速增量的秘密
Bazel 有一个极其震撼的特性及效果:在多个大型的 Bazel 工程中,当无任何代码修改的情况下,Bazel 能够在 1s 内执行完毕。这个对于研究 Gradle 的人来讲太震撼了,Gradle 在未执行任何修改的情况下肯定是做不到这个效果的。接下来我们看看两个构建系统是如何实现快速增量编译的,为了方便阐述,我们就以改动少量代码的场景来进行说明。
假设已经进行了一次全量编译,也就意味着有了一张全量的 DAG,此时改动少量代码,假设改动影响到的是 T5,毫无疑问,T5 是肯定会执行的。
上文中讲到,Gradle 是一个高度灵活的构建系统,此外其还是一个单进程的构建系统,Task 间没有严格的进程级别的隔离机制,导致 Task 间可能存在访问关系,所以无法提前判断到底应该执行哪些 Task。
因此 Gradle 会在执行阶段进行全量判断,也就是会遍历每个 Task 是否需要执行。如图中的 T2、T4 及 T6 的部分,直观的感受是大概率和 T5 并没有任何关系,但依然需要做一次判断。
所以 Gradle 要想提升增量构建的性能,必然需要在判断是否需要执行的逻辑上做足功夫,确保每个节点的判断迅速完成,否则无法应对大型工程的构建。Gradle 的应对法宝为:
-
Daemon 进程 -
虚拟文件系统(Virtual File System,后文简称 vfs) -
远程缓存
首先采用 Daemon 进程 来进行全局的内存缓存,然后对于每个 task 的输入输出变更应用了 vfs 监控来快速进行检测,同时对于每个 Task 还用远程缓存进行兜底,来保证快速并发检测多个节点。绝大部分的节点的检测均控制在 10ms 左右完成,性能方面已经比较出色。以抖音 Android 为例,一次构建大概需要 14000 个 Task,全部判断完成大概需要 10s 左右,这个时间针对于增量构建所需的任务来讲,并不算长。
再来看看 Bazel 的视角如何做到快速的增量编译。与 Gradle 相比,相同点是均采用 Daemon 进程 + vfs 进行快速的变更检测,不同点在于 Bazel 的 DAG 设计。依然是产物驱动型
的理念带来的优势,使得 Bazel 建立的 DAG 基本更细的颗粒度。几乎可以认为任何关系均可以从该 DAG 中获取。
对示例中的流程来讲:
-
我们修改了一部分代码,全局的 vfs 能够快速感知变更的文件 A.java ,假设修改的是A.java文件。 -
利用全局的 DAG 索引,通过 T5 = graphNodeMap.get(“A.java”),直接获取该文件从属于节点 T5 -
依然利用 DAG 索引,通过递归调用 getReverseDeps 依次找到依赖 T5 的 T3 和 依赖 T3 的 T1,并将它们标记为脏节点。其余的T2 、T4和T6则被判定为无需执行。实际标记的过程远比描述的复杂,这里只进行理念的阐述。 -
调度器将只会执行剪枝后的 DAG,即 T5 – T3 – T1,规模迅速缩小
总结
Bazel 通过全局的 DAG 索引保证增量过程中总是执行“最小 DAG”,执行规模不随工程规模的扩大而线性增长,这个特性也很大程度上决定了 Bazel 能够应对超大仓的挑战;
而 Gradle 可能会随着工程规模增量效率出现劣化,但其增量性能依然比较出色,面对抖音 Android 的 Monorepo,单次构建超过 14000 个 Task,依然可以在 5 – 10s 内完成所有增量 Task 判断,也算表现不俗。
无论是并发或者是 Exectution 阶段的增量效果,虽然 Bazel 更好,但实际上并没有很大的区别。不过接下来我们要介绍的配置阶段可能差别就比较大了。
配置阶段的巨大差异
如果单纯从性能角度对比,analysis 阶段或者叫 configuration 阶段二者的性能差异是最明显的。Bazel 几乎处于吊打 Gradle 的地步。注意观察图中绿色框的部分,这个是设计上的核心差距。
从上图可以得知二者都有 DAG 来指导编译流程的执行,但在生命周期和效率上有较大区别。
-
Gradle 的 DAG 只为执行阶段服务,configuration 阶段仅仅是为了生成待执行的 DAG; -
Bazel 的 DAG 是真正的全生命周期,覆盖了 Analysis 阶段。这就意味着上一节提到的”剪枝的 DAG“的优势复用到了 Analysis 阶段,换句话说全生命周期的各个阶段均能享受到同样的缓存能力、增量能力。
Gradle 的 configuration 阶段可以认为是整个 Gradle 构建系统最不尽如人意的设计。在业界很多人不敢做 Android 全源码很大程度上也是因为担心 configuration 过程时间就炸了。这里应该算是 Gradle 的一个设计缺陷,在早期过于考虑灵活性,动态的 groovy 语言加上过于开放的 API,导致 Configuration 阶段难以做高质量的缓存及更高程度的并发。后来官方意识到这个问题后,采用了一种及其激进的缓存方案,称为 Configuration cache。原理很暴力,“既然大部分场景不涉及配置改动,直接将整个 DAG 进行序列化缓存,如果不改动配置,就直接反序列化回来”。这里面涉及两个问题:
-
改了配置的场景,依然龟速 -
没改配置的场景,跳过了过多步骤,导致改造成本非常高,对于很多已存在的大型项目来讲均有较大的挑战
虽然官方在非常努力的演进这个 feature,已经横跨N个版本,但无论怎么样都像是一种“亡羊补牢”的打补丁的方案。
再来看看 Bazel,Bazel 在设计之初进行了充分的思考,在性能方面做足了功课。在 Bazel 的世界里,一切都可以简单的抽象成一个函数模型:输入 x 通过一个函数得到 y,并且要保存下来所有的依赖关系,比如可以轻松的通过 x 查询到 y 的状态。基于这个模型设计好 DAG 和缓存,就能实现全生命周期的覆盖,就无所谓区分 Analysis 和 Execution 阶段了,二者均可以享受增量缓存和“DAG 剪枝”的效果了。
Bazel 先进的设计理念将全生命周期一体化抽象,在 Analysis 阶段确实要比 Gradle 出色太多,或者说在这个层面二者就不是一个 level 上的选手。
其他核心能力
分布式编译
对于分布式编译能力,两套系统出现了分歧,分布式编译一直是 Bazel 的一个核心“卖点”,而 Gradle 没有分布式能力且官方未来也没计划跟进。
由于 Java 编译本身就比较轻量,加上没有头文件加持,很难做到单文件粒度的编译,意味着分布式编译不见得就能带来很大的收益。这么看来分布式编译能力在 JVM 体系下貌似并不算刚需,而 Android 编译瓶颈在于长尾效应非常严重(如下图),这也是为什么在 Google 内部构建一个 Android Release 包依然很慢的核心原因,这个慢当前来看并不取决于构建系统自身的性能。
分布式编译固然能吸引眼球,高大上,但不一定能解决问题,Gradle 没有分布式编译能力好像并没有影响什么。但其对 C 系编译的作用还是很大的。
依赖管理能力
Gradle 几乎全部继承了 Maven 在依赖管理方面的优势并进行了极致的优化,这对于复杂项目和超大型项目而言至关重要。而 Bazel 在依赖管理能力就显得有点不入流了,这和背景有一定的关系,因为 Bazel 诞生于 google 的超大仓,不需要远程依赖,甚至不需要版本决议。开源后发现玩不转,补了个 rules_jvm_external 来管理外部依赖,感受上大概率抄袭了 Gradle 的一些东西,但能力依然很弱。最近在做的 bzlmod 可能会好一些,这块显然是 Bazel 的短板,被 Gradle 甩了几条街。
值得说明的一点是:依赖解析的过程很大程度上会影响 Analysis 阶段的耗时,这也是为什么看似 Bazel 的设计更优秀,但实际上我们测试的结果显示,在全量编译及修改代码的场景,Bazel 在 Analysis 阶段也没有想象中的那么快,并没有完全发挥出设计优势。
以上是针对 Bazel 与 Gradle 构建系统层面的对比,整体下来确实是 Bazel 的设计更加优秀,但为什么 Bazel 在 Android 方面或者 Java 领域规模很小呢,接下来我们再从 Android 构建的角度来简单的描述下。
Android 构建
详细对比完 Gradle 和 Bazel 在多个维度的差异,这章节会围绕 Android 构建,从构建性能、生态两方面来陈述下。
构建性能
首先我们需要明确的是一个构建任务是否能高效完成,并不完全由构建系统决定。并不是说“Bazel 比 Gradle 设计的更出色,用 Bazel 构建 Android 就比 Gradle 要快”。Android 构建过程相对复杂,需要如下几个基础能力配合完成。
Android Gradle Plugin: 简称 AGP,由 Android 官方团队维护开发,投入力度较大,虽然性能方面还有很多提升空间,但功能完整性很高。对应的 Bazel 体系则为 rules_android,Bazel 的 rules_android 功能层面极其粗糙,由开源社区维护,近两年的活跃度很低,相较于 AGP 来讲,rules_android 无论从性能和完善度方面相较于 AGP 均有较大的差距。AGP 和 rules_android 对于构建体验的重要性甚至超过构建系统自身的性能。
Kotlin Gradle Plugin:简称 KGP,kotlin 官方维护开发,更新也较频繁,针对 kotlin 编译做了比较多的优化。对应到 Bazel 体系则为 rules_kotlin。rules_kotlin 由社区维护,从能力和稳定性来讲全面弱于官方 KGP。
至于 java 编译部分,Gradle 就更狠了,Gradle 内置了 java 插件,并且在 Gradle 框架层面进行了较为极致的优化。针对 java 编译,Gradle 其实是“不惧”Bazel 的。这里其实依赖一个设计理念的区别。
Gradle 是有增量 Task 的概念的,而 Bazel 没有增量 Action 的设计。换言之,Gradle 从设计上是支持自定义增量 Task,构建系统层面感知变更,Task 实现者来实现增量 Task,虽然写好增量 Task 并不是件容易的事,但这对 Gradle 体系极为重要。Bazel 并没有类似的机制,它的最小的缓存单元就是 Action,Bazel 的理念是“定义的越细,性能就越好”。
举个例子来描述下 Gradle 细粒度增量编译能力
└── java
└── com
├── A.java
├── B.java
├── C.java
└── util
├── D.java
├── E.java
└── F.java
在大型项目中,一个模块具备十几个文件夹、数百个类是一件很常见的场景,从设计的角度来看也是合理的。Gradle 针对 java 编译做了非常极致的优化,如上图中,如果 D 变了,可以做到只编译 D。而 Bazel 体系下,构建单元完全是由 BUILD 文件来确定。换句话说,如果类似 Gradle 只在模块的根目录下配置 BUILD 文件,则会同时编译所有文件,推荐的解决办法是在 util 文件夹下配置单独的 BUILD 文件,这样 D、E、F 会被看做一个独立的执行单元,修改 D 文件,就会变为编译 D、E、F。其实这是一种理念的差异。当在符合各自构建系统的理念的使用姿势下,均可以达到良好的构建性能。但 Bazel 的理念和使用姿势在 JVM 领域显得有一些苛刻和难以接受。
所以对于 java 构建来讲,在增量阶段,Gradle 的性能是极其突出的,绝大部分场景是要快于 Bazel 的。
Benchmark
文章到此处还未出现数据层面的对比。因为针对 Android 项目,在业界确实找不到相对公平比对的 benchmark 项目,甚至想改造出一个双系统进行跑通的中等工程都很困难。注意,我们希望用的是真实项目,demo 级别的对比或者脚本生成的工程对比其实并没有较大的意义。为此,我们进行了一个真实项目的改造选取了飞书 Docs 项目约 80 个模块搭建了 Gradle 和 Bazel 的双构建系统以对比二者的差异。
实验环境:
-
飞书 Docs 项目 80 个 module -
Bazel 版本:6.2.1 -
Gradle 版本:6.7.1 | AGP 版本:4.1.0 -
Gradle 与 Bazel 同等配置粒度 ,粒度较粗,从 Bazel 的理念上来看,Bazel 略吃亏 -
Gradle 方面没有去掉 Debug 阶段耗时的 Transform 环节,Bazel 原生并没有支持此能力,从这方面看,对 Gradle 略不公平
增量编译场景结果如下:
Gradle | Bazel | explanation | |
---|---|---|---|
NoChange | 20s | 0.222s | 验证了上文提到的 Bazel 设计方面的特性。VFS+DAG 剪枝 |
底层模块 ABI Change | 35s | 95.8s | Bazel 不具备增量 Action 的能力,级联变更较多时性能很差。Gradle 的增量 Task 发挥了巨大作用 |
底层模块 NonABI Change | 35s | 18.5s | Bazel 模块间同样具有“编译避免”的能力,加上 analysis 阶段的优势,整体耗时较短。 |
上层模块 ABI Change | 30s | 31.5s | 与底层模块 ABI Change 类似 |
上层模块 NonABI Change | 30s | 16.6s | 与底层模块 NonABI Change 类似 |
可以看出在增量编译场景下,由于 Bazel 配置阶段的巨大优势使它在 NoChange 和 NonABI Change 场景下大幅领先 Gradle,然而在 ABI Change 场景下,Gradle 细粒度的增量能力与更完善的编译避免能力发挥了作用,反杀了 Bazel。
全量编译场景结果如下:
Gradle | Bazel | |
---|---|---|
有缓存 | 42s | 31.2s |
无缓存 | 120s | 969.078s(优化后 248s) |
而在全量编译场景下,Bazel 的表现只能用惨不忍睹来形容了,我们团队也针对这一难以置信的数据表现进行了细致分析,并做了一些优化,优化后的时长降低到了 248s。其主要原因是很多工作量小,数量极大的 Action 没有以 Worker (https://bazel.build/remote/persistent?hl=en)的方式执行,而导致大量进程创建开销(Worker 可以使多个 Action 复用同一个常驻进程来执行,避免每次执行都创建新进程),比如从 AAR 中提取产物,以及安卓资源处理方面的一些缺陷,如 AAPT2 没有使用 Daemon 模式,冗余的 Link 调用等等,这也印证了 rules_android 确实还不够完善,我们也将一些通用优化向 Bazel 官方提了 PR,其优化方案也得到了官方的认可:
-
https://github.com/Bazelbuild/Bazel/pull/18496 -
https://github.com/Bazelbuild/Bazel/pull/18573 -
https://github.com/Bazelbuild/rules_jvm_external/pull/911
生态
在研发阶段最影响用户体验的两个环节是 IDE 和 构建系统相关的工具链体系。目前 Android 开发唯一的 IDE 即 Android Studio 也是由 Google Android 官方团队维护的。从官方信息来看,Android Studio 与 AGP 越来越倾向于强耦合,虽然理论上 Android Studio 和 Gradle 之间并不存在强耦合的关系,任何构建系统都可以通过自行实现 IDE 扩展来支持开发的能力,但不可否认的是 Android Studio 与 Gradle 系统的适配是官方进行开箱即用的,Bazel 的 AS 支持只能由 Bazel 团队及社区完成,对于新特性的支持显然是要滞后一大截,甚至是否能对齐都有待商榷,稳定性存疑。
从生态上看,Bazel 的 Android 生态与 Gradle 对比极其弱小,并未得到官方的强力支持,加上 Gradle 和 Bazel 体系玩法相差巨大,想低成本改造绝非易事。好在 Bazel 对于 Android 也有进一步的规划,在 2023 年的 Bazel Roadmap 里表明,Android Rules 将由 Starlark 重写,迁移出 Bazel 源码,由 Bazel 官方和社区一起维护 Android Rules。不过想追平现有的 Android 工具链生态,只能说任重而道远。
2023 年 Bazel Roadmap :https://bazel.build/about/roadmap?hl=zh-cn#bazel_ecosystem_tooling
我们能做什么
构建系统的性能决定了构建性能的上限,生态体系决定了下限。长远来看,朝着 Monorepo 的演进,Bazel 的上限更高,但就目前及中短期来看,受限于生态及工具链匮乏,它的下限也很低。Buildinfra 团队对 Bazel 从源码及工具链层面进行了较为深入的研究。目前 Android 层面通过在 Gradle 场景的优化,还未触达 Gradle 体系的上限,但不代表未来不会,所以我们会对 Bazel 进行适当的长期的投入,并对社区做出一定的贡献。
与此同时,我们是少有的能同时深入两个超大型构建系统的团队,完全可以借鉴 Bazel 的优异设计来反哺当前 Gradle 生态。能够达到横向迁移的目的,也能体现当下的价值,我们在研究 Bazel 的过程中确实受到了不少的启发,并已经迁移到了 gradle 上。
举个例子:
-
前文提到 Bazel 对于增量构建来讲,DAG 剪枝的能力极其诱人,这也是能做到增量编译速度不随工程规模劣化的原因,注意这里的劣化指的是不引入过多的自身损耗。比如 DAG 的节点有 1000 个,修改一行只需要执行 100 个,其余的不用参与构建。当节点扩展到 10000 个时,同样的修改,也只有 100 个需要执行。而 gradle 体系则不然,如果工程规模很大,那 Task 的个数就会线性增长,比如 1000 个时,修改一行代码,真正需要重新运行的 task 同样为 100 个,但要进行 990 个 Task 是否能复用缓存的判断。虽然他的判断极其高效,但这个数量级会随工程规模线性增长,当 10000 个 task 时,就会进行 9900 个 task 的判断。这也是为什么我们不看好 Gradle 能成为巨型仓库的构建系统的原因。
但是 Bazel 的这个能力太诱人了,那我们有没有办法在 Gradle 上也进行一次 task 的剪枝呢,为此我们团队做了一套剪枝的方案,对于抖音全源码工程来讲,几乎砍掉了 90%的 task 的判断。这完全来源于 Bazel 的启发。
总结
本文首先从构建系统设计和理念的角度对 Bazel 和 Gradle 进行了深度的对比,然后围绕 Android 构建方面的表现从性能和生态两个角度进行了阐述,最后讲述了一下我们的研究思路与优化的能力。
总的来说:
-
对于超大型或者巨型工程来讲,Bazel 确实是独一无二的选择,优秀的理念和设计上限更高。但这个超大型如何定义呢,以我们在 Monorepo 的实践来看,大概量级是在抖音 Android 现有规模的 2-3 倍左右,注意这里指的是单体 app 规模。现有规模 Gradle 经过优化的表现依然有着较大的承载空间。 -
Bazel 在当前 Android 构建领域不够完善且缺乏官方支持,长期来看,是否能让生态成长起来还有较大的不确定性,毕竟不是所有的项目都是超大型项目,对于中小型项目无论从易用性、性能和生态的角度都几乎处于被吊打的状态。
对于 Bazel 构建系统,我们会进行持续关注与投入,后续为大家带来更多 Android 视角的看法与思考!近期我们也会将字节大型 Android 项目在 Monorepo 全源码模式改造过程中的一些经验和沉淀分享给大家,敬请期待!
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=15338,转载请注明出处。
评论0