天天看點

Android 視角談 Bazel 與 Gradle 建構系統

作者:閃念基因

本文由 AppInfra-Build 團隊出品。作者:蘭軍健、丁德高、謝然

本文是從建構系統對比的角度出發,深度的對比了Gradle與Bazel兩大建構系統的設計理念及優劣勢,并結合Android 建構的表現進行了詳細的分析

背景

目前位元組 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 項目,同樣支援多語言。

一個成熟的建構系統的核心要素大概包括以下幾個次元:

Android 視角談 Bazel 與 Gradle 建構系統

核心排程機制: 建構系統的“發動機”,排程能力的設計一定程度上決定了建構系統的上限。

建構規則 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 均具備了大型建構系統的核心要素,接下來我們逐漸深入,從更細的次元來進行下對比,在進入之前先來看看兩者在建構流程方面的差異,友善大家了解下文中的一些概念。

建構流程

當我們執行一條建構指令時,建構系統基本上都會經曆三個階段來完成建構。

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 年開源。

二者的設計理念和自我定位有着較大差別:

理念解釋BazelArtifact-based build system産物驅動型隻聲明需要什麼,依賴什麼産物;Bazel 會自動依賴産物來關聯相關的 action,這些 action 是否并發執行也僅取決于産物的依賴情況。舉個例子:定義 ActionX和 ActionY,ActionX input 中依賴了一個 A.jar ,ActionY 會産出 A.jar 産物,則 ActionX 會自動隐式依賴 output 中含有 A.jar 的 ActionY。GradleTask-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 驅動型相對來講就會有些吃力。
  • 理念的差異其實一定程度也會影響到建構系統的并發能力;産物驅動型的建構系統的并發能力一定程度上會更高。

這裡了解起來可能還是有點抽象,下面的章節中會不斷地在示例中闡述這種裡面層面的差異。接下來先來看看并發能力方面的差異。

并發能力

并發能力是衡量一個建構系統的核心名額,我們通過一個最小化的場景來對比一下。

Android 視角談 Bazel 與 Gradle 建構系統

如圖所示,假設有三個任務 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 是肯定會執行的。

Android 視角談 Bazel 與 Gradle 建構系統

上文中講到,Gradle 是一個高度靈活的建構系統,此外其還是一個單程序的建構系統,Task 間沒有嚴格的程序級别的隔離機制,導緻 Task 間可能存在通路關系,是以無法提前判斷到底應該執行哪些 Task。

是以 Gradle 會在執行階段進行全量判斷,也就是會周遊每個 Task 是否需要執行。如圖中的 T2、T4 及 T6 的部分,直覺的感受是大機率和 T5 并沒有任何關系,但依然需要做一次判斷。

是以 Gradle 要想提升增量建構的性能,必然需要在判斷是否需要執行的邏輯上做足功夫,確定每個節點的判斷迅速完成,否則無法應對大型工程的建構。Gradle 的應對法寶為:

  • Daemon 程序
  • 虛拟檔案系統(Virtual File System,後文簡稱 vfs)
  • 遠端緩存
Android 視角談 Bazel 與 Gradle 建構系統

首先采用 Daemon 程序 來進行全局的記憶體緩存,然後對于每個 task 的輸入輸出變更應用了 vfs 監控來快速進行檢測,同時對于每個 Task 還用遠端緩存進行兜底,來保證快速并發檢測多個節點。絕大部分的節點的檢測均控制在 10ms 左右完成,性能方面已經比較出色。以抖音 Android 為例,一次建構大概需要 14000 個 Task,全部判斷完成大概需要 10s 左右,這個時間針對于增量建構所需的任務來講,并不算長。

再來看看 Bazel 的視角如何做到快速的增量編譯。與 Gradle 相比,相同點是均采用 Daemon 程序 + vfs 進行快速的變更檢測,不同點在于 Bazel 的 DAG 設計。依然是産物驅動型的理念帶來的優勢,使得 Bazel 建立的 DAG 基本更細的顆粒度。幾乎可以認為任何關系均可以從該 DAG 中擷取。

Android 視角談 Bazel 與 Gradle 建構系統

對示例中的流程來講:

  • 我們修改了一部分代碼,全局的 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,規模迅速縮小
Android 視角談 Bazel 與 Gradle 建構系統

總結

Bazel 通過全局的 DAG 索引保證增量過程中總是執行“最小 DAG”,執行規模不随工程規模的擴大而線性增長,這個特性也很大程度上決定了 Bazel 能夠應對超大倉的挑戰;

而 Gradle 可能會随着工程規模增量效率出現劣化,但其增量性能依然比較出色,面對抖音 Android 的 Monorepo,單次建構超過 14000 個 Task,依然可以在 5 - 10s 内完成所有增量 Task 判斷,也算表現不俗。

無論是并發或者是 Exectution 階段的增量效果,雖然 Bazel 更好,但實際上并沒有很大的差別。不過接下來我們要介紹的配置階段可能差别就比較大了。

配置階段的巨大差異

如果單純從性能角度對比,analysis 階段或者叫 configuration 階段二者的性能差異是最明顯的。Bazel 幾乎處于吊打 Gradle 的地步。注意觀察圖中綠色框的部分,這個是設計上的核心差距。

Android 視角談 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 剪枝”的效果了。

Android 視角談 Bazel 與 Gradle 建構系統

Bazel 先進的設計理念将全生命周期一體化抽象,在 Analysis 階段确實要比 Gradle 出色太多,或者說在這個層面二者就不是一個 level 上的選手。

其他核心能力

分布式編譯

對于分布式編譯能力,兩套系統出現了分歧,分布式編譯一直是 Bazel 的一個核心“賣點”,而 Gradle 沒有分布式能力且官方未來也沒計劃跟進。

由于 Java 編譯本身就比較輕量,加上沒有頭檔案加持,很難做到單檔案粒度的編譯,意味着分布式編譯不見得就能帶來很大的收益。這麼看來分布式編譯能力在 JVM 體系下貌似并不算剛需,而 Android 編譯瓶頸在于長尾效應非常嚴重(如下圖),這也是為什麼在 Google 内部建構一個 Android Release 包依然很慢的核心原因,這個慢目前來看并不取決于建構系統自身的性能。

Android 視角談 Bazel 與 Gradle 建構系統

分布式編譯固然能吸引眼球,高大上,但不一定能解決問題,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 視角談 Bazel 與 Gradle 建構系統

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 略不公平

增量編譯場景結果如下:

GradleBazelexplanationNoChange20s0.222s驗證了上文提到的 Bazel 設計方面的特性。VFS+DAG 剪枝底層子產品 ABI Change35s95.8sBazel 不具備增量 Action 的能力,級聯變更較多時性能很差。Gradle 的增量 Task 發揮了巨大作用底層子產品 NonABI Change35s18.5sBazel 子產品間同樣具有“編譯避免”的能力,加上 analysis 階段的優勢,整體耗時較短。上層子產品 ABI Change30s31.5s與底層子產品 ABI Change 類似上層子產品 NonABI Change30s16.6s與底層子產品 NonABI Change 類似

可以看出在增量編譯場景下,由于 Bazel 配置階段的巨大優勢使它在 NoChange 和 NonABI Change 場景下大幅領先 Gradle,然而在 ABI Change 場景下,Gradle 細粒度的增量能力與更完善的編譯避免能力發揮了作用,反殺了 Bazel。

全量編譯場景結果如下:

GradleBazel有緩存42s31.2s無緩存120s969.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

生态

Android 視角談 Bazel 與 Gradle 建構系統

在研發階段最影響使用者體驗的兩個環節是 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 全源碼模式改造過程中的一些經驗和沉澱分享給大家,敬請期待!

作者:AppInfra-Build

來源:微信公衆号:位元組跳動終端技術

出處:https://mp.weixin.qq.com/s/4AI7H428oSc4fWgcK3KOpQ

繼續閱讀