Monorepo 解決方案 — 基于 Bazel 的 Xcode 性能優(yōu)化實(shí)踐
背景介紹
書接上回《Monorepo 解決方案 — Bazel 在頭條 iOS 的實(shí)踐》,在頭條工程切換至 Bazel 構(gòu)建系統(tǒng)后,為了支持用戶使用 Xcode 開發(fā)的習(xí)慣,我們使用了開源項(xiàng)目 Tulsi 作為生成工具,用于將 Bazel 工程轉(zhuǎn)換為 Xcode 工程。但是在使用的過程中,我們發(fā)現(xiàn)了一些問題,其中影響較大的是,
- Xcode 工程卡頓:對(duì)于頭條這種大型項(xiàng)目來說,Xcode 卡頓一直是本地研發(fā)的痛點(diǎn)問題,在切換 Bazel 構(gòu)建系統(tǒng)之后卡頓現(xiàn)象明顯加劇。
- Xcode 功能支持受限:Tulsi 支持的功能有限,很多功能都年久失修,并未持續(xù)適配 Bazel 與 rules 的更新。
在做了一些前期調(diào)研后,我們發(fā)現(xiàn) rules_xcodeproj 提供了更好的解決方案。rules_xcodeproj 是一個(gè)由一系列 Bazel rules 組成的開源項(xiàng)目,使用它可以從一個(gè) Bazel 工程生成對(duì)應(yīng)的 Xcode 工程,實(shí)現(xiàn)在 Xcode 中寫代碼同時(shí)使用 Bazel 進(jìn)行真正的構(gòu)建任務(wù),相比于 Tulsi,rules_xcodeproj 在以下幾個(gè)方面有著更為明顯的優(yōu)勢(shì)。
- Xcode 工程更加流暢: 頭條工程遷移到 rules_xcodeproj 后,工程首次冷啟、二次啟動(dòng)和文件增刪操作的時(shí)間有了明顯縮短,工程卡頓情況也明顯好轉(zhuǎn)。
- Xcode 功能支持度更高: rules_xcodeproj 對(duì) Xcode 的支持更全面(包括單元測(cè)試、SwiftUI Previews),能夠更好地滿足我們的需求。
- 社區(qū)更加活躍:隨著 rules_xcodeproj 在 2023 年 2 月份發(fā)布正式版,Tulsi 項(xiàng)目也正式宣布停止維護(hù),這意味著對(duì)于新版本的 Bazel,Tulsi 將不再提供適配和支持;同時(shí) rules_xcodeproj 幾乎每月都會(huì)更新一個(gè)中版本,對(duì)于后續(xù)適配 Bazel 更新的成本會(huì)更低。
- 更符合 BitSky 的演進(jìn)路線:由 Bazel 驅(qū)動(dòng)的工程生成方式更符合 BitSky 的 Bazel Native 演進(jìn)路線,可以完全在 Bazel 環(huán)境下生成工程。
因此,我們將 Xcode 生成工具從 Tulsi 遷移到了 rules_xcodeproj,并對(duì) BitSky 工具鏈進(jìn)行了適配。適配過程中,我們?cè)谛迯?fù) rules_xcodeproj 索引問題的同時(shí),對(duì)工程結(jié)構(gòu)進(jìn)行了一些優(yōu)化,進(jìn)一步提升了工程流暢度。頭條 iOS 工程的測(cè)試數(shù)據(jù)如下:
測(cè)試設(shè)備 MacBook Pro,芯片 M1 Pro,內(nèi)存 32GB
ps. 本文介紹均基于 rules_xcodeproj 1.4.0 版本,build with bazel 模式
pps. 由于 Xcode 主線程的卡頓較難監(jiān)測(cè),此處用主動(dòng)操作的執(zhí)行時(shí)間衡量流暢度
Tulsi | rules_xcodeproj | |
工程首次冷啟 | 47s | 16s |
二次啟動(dòng) | 33s | 12s |
文件操作 | 新增 20s 刪除 23s | 新增 8s 刪除 6s |
下面來看下我們具體做的適配工作。
適配過程
從 Tulsi 遷移到 rules_xcodeproj 后,我們發(fā)現(xiàn) Xcode 卡頓有明顯改善,仔細(xì)分析發(fā)現(xiàn) Tulsi 工程的卡頓主要有兩方面原因:
- 頭條工程中組件數(shù)量多、依賴關(guān)系復(fù)雜:Tulsi 的索引方案需要 Xcode Target 之間保留這些依賴關(guān)系,但這些依賴關(guān)系并不會(huì)被 Xcode 構(gòu)建消費(fèi),卻增加了 Xcode 對(duì)工程進(jìn)行解析和計(jì)算的成本。
圖中 Pod X_A 與 Pod X_B 為 Pod X 分別在 App A 與 App B 下被引用的 Target
- 全源碼構(gòu)建:我們?cè)谇袚Q Bazel 時(shí)還推進(jìn)了工程的全源碼構(gòu)建,源碼數(shù)量大幅增加,也給索引任務(wù)帶來更大的壓力。
接下來詳細(xì)說明整個(gè)分析和適配過程,帶大家更全面地了解我們結(jié)合 rules_xcodeproj 在 Xcode 背后做了什么。
在 Xcode 中開發(fā)時(shí)主要會(huì)用到三部分功能:構(gòu)建、索引和調(diào)試。而 Xcode 并不能直接理解 Bazel 項(xiàng)目的工程文件(BUILD 和 WORKSPACE),所以我們需要通過工具(rules_xcodeproj 或 Tulsi)將 Bazel 的工程文件轉(zhuǎn)換為 Xcode 可以理解的 .xcodeproj
來支持這些功能正常工作。
各功能的適配點(diǎn)如下表所示。
rules_xcodeproj 原生方案的調(diào)試模塊基本能正常工作,而索引和構(gòu)建兩個(gè)模塊中我們對(duì)原生方案的改造較大,因此本文主要對(duì)這兩個(gè)模塊進(jìn)行展開介紹。
索引
前面提到支持 Xcode 索引功能需要提供各個(gè)源文件的編譯參數(shù),rules_xcodeproj 實(shí)現(xiàn)這一點(diǎn)的工作流程是:
- rules_xcodeproj 在 Bazel 的分析階段獲取到源碼文件和編譯參數(shù);
- 用這些信息創(chuàng)建 Xcode Target 的 Compile Sources 和 Build Setting 產(chǎn)出工程文件
.xcodeproj
; - Xcode 加載工程文件,獲取源碼文件對(duì)應(yīng)的索引參數(shù),調(diào)用 clang 或 swiftc 執(zhí)行索引命令。
遷移過程中我們注意到 rules_xcodeproj 工程的索引在多 Target 共用源文件的場(chǎng)景存在語法高亮異常的問題,對(duì)比 Tulsi 工程的處理方式后我們得出兩個(gè)結(jié)論:
- 索引異常是由于 rules_xcodeproj 移除了所有 Library Target 間的依賴導(dǎo)致的;
- Tulsi 工程中 Library Target 間的依賴關(guān)系正是導(dǎo)致 Tulsi 工程更為卡頓的關(guān)鍵原因。
后文會(huì)先分析依賴關(guān)系在索引中發(fā)揮的作用,以及 rules_xcodeproj 如何處理依賴關(guān)系移除帶來的問題,然后介紹我們?nèi)绾瓮ㄟ^源碼合并方案解決 rules_xcodeproj 索引方面的缺陷。
依賴關(guān)系在索引中的作用
這里提到的“依賴關(guān)系”主要是指 Xcode 工程文件(.xcodeproj
,準(zhǔn)確來說是其中的project.pbxproj
文件)中記錄的 Xcode Target 之間的依賴關(guān)系。
需要明確的是這部分依賴關(guān)系只會(huì)在 Xcode 索引過程被用到,被移除后也只會(huì)影響 Xcode 的索引功能。構(gòu)建時(shí)用到依賴關(guān)系 Bazel 會(huì)從 BUILD 文件中獲取,不會(huì)關(guān)心 .xcodeproj
中的信息。
這些依賴關(guān)系在 Xcode 中的表現(xiàn)形式如下圖所示,既有直接在 Build Phases 的 Target Dependencies 中聲明的依賴,也會(huì)對(duì)聲明在 Link Binary With Libraries 中 Target 產(chǎn)生依賴。
概括來說依賴關(guān)系在 Xcode 的索引過程中發(fā)揮著以下兩方面作用:
- 正確的中間產(chǎn)物生成順序:clang/Swift Module 中間產(chǎn)物的生成需要依賴關(guān)系來確定構(gòu)建順序;
- 正確的多 Target 編譯參數(shù):多 Target 共用源文件時(shí)應(yīng)用正確的編譯參數(shù),以便于正確地高亮代碼。
下面分別對(duì)其進(jìn)行展開介紹。
中間產(chǎn)物生成順序
以一個(gè) Swift 組件為例,當(dāng)它被 OC 組件引用時(shí),需要生成一個(gè) XX-Swift.h 文件,把方法和聲明暴露給 OC 組件,當(dāng)它被其他 Swift 組件引用時(shí),也需要生成一個(gè) swiftmodule 文件供其他 Swift 組件引用。XX-Swift.h 和 swiftmodule 并不是原始的源碼文件,也不是最終的二進(jìn)制產(chǎn)物,是構(gòu)建時(shí)的中間產(chǎn)物。在 Xcode 對(duì)一個(gè)組件的源代碼索引時(shí),需要這個(gè)組件的依賴組件已經(jīng)準(zhǔn)備好中間產(chǎn)物供索引時(shí)消費(fèi)。這里,我們先分析下 Xcode 是怎樣解決這個(gè)問題的。
首先,Xcode 通過 Target 來描述產(chǎn)物是如何構(gòu)建出來的,每個(gè) Target 擁有自己的 Build Phases 和 Build Settings(通常一個(gè)組件對(duì)應(yīng)一個(gè) Target)。Build Phases 中的 Compile Sources 記錄了構(gòu)建這個(gè) Target 需要編譯哪些源碼文件。Build Settings 里記錄了構(gòu)建這個(gè) Target 時(shí)的各種配置,這其中就包括了編譯參數(shù)。Xcode 可以從 Build Settings 里去獲取編譯參數(shù),然后對(duì) Compile Sources 中記錄的源碼文件進(jìn)行索引編譯。
一個(gè) Target 引用另外一個(gè) Target 時(shí),需要將依賴關(guān)系添加到 Build Phases 的 Target Dependencies 中。Xcode 在構(gòu)建時(shí)會(huì)根據(jù)這些依賴關(guān)系來決定構(gòu)建 Target 的順序,保證一個(gè) Target 構(gòu)建時(shí),它依賴的 Target 已經(jīng)完成構(gòu)建。
索引時(shí)也是類似的處理,Xcode 有一個(gè) Index Build 的階段,在這個(gè)階段也會(huì)根據(jù)依賴關(guān)系按照順序去觸發(fā) Target 的 Build Phase,完成之后才會(huì)開始索引這個(gè) Target 的源碼文件。
這里有一個(gè)例子,SwiftDemo 依賴了 HelloLib,兩個(gè) Target 均為 Swift 組件。
在對(duì) SwiftDemo 的源碼文件進(jìn)行索引編譯之前,會(huì)先觸發(fā) Index Build。Index Build 時(shí)根據(jù)依賴關(guān)系,先 Build HelloLib,這個(gè)時(shí)候會(huì)進(jìn)行 Compile Swift source files,參數(shù)中包含 -emit-module -emit-module-path /path
,最終會(huì)生成 swiftmodule 。
如果將 HelloLib 從 SwiftDemo 的 Target Dependencies 中移除,在 SwiftDemo 的 Index Build 時(shí),不會(huì)先 Build HelloLib,并且在 HelloLib 的 Index Build 中,也不會(huì)生成 swiftmodule。
可以看出,Xcode 正是通過依賴關(guān)系來保證構(gòu)建時(shí)的順序,索引也依賴構(gòu)建順序來保證 Module 中間產(chǎn)物的生成時(shí)機(jī)。
在 Tulsi 生成的 Xcode 工程中,依然保留了 Target 之間的依賴關(guān)系,用于解決索引的中間產(chǎn)物生成問題。這里就不做過多介紹。
而在 rules_xcodeproj 生成的工程中,Target 之間的依賴關(guān)系是完全去掉的,每個(gè) Target 都有且僅有一個(gè)額外添加的依賴 BazelDependencies。對(duì)于 Module 中間產(chǎn)物的處理,在生成的 Xcode Target 中,我們可以看到這樣一條 Build Phase。
原理是在 Index Build 階段,執(zhí)行到這 Target 時(shí)去跑一個(gè) shell 腳本。
以 NewsInHouse 這個(gè) Target 為例,這個(gè)腳本里經(jīng)過一系列的處理,最終會(huì)去調(diào)用這樣一條 Bazel Build 命令,
在這條命令中有一些關(guān)鍵信息:
@rules_xcodeproj_generated//generator/xcodeproj:xcodeproj
Bazel Build 的 Target,可以認(rèn)為是我們使用 rules_xcodeproj 定義的生成工程的 Bazel Target。
bc //Article:NewsInHouse applebin_ios-*
在上述 Target 里 rules_xcodeproj 添加了一些 OutputGroupInfo。Bazel Build 時(shí)可以通過 --output_groups 參數(shù)指定輸出產(chǎn)物。
這一條 output group 對(duì)應(yīng)的是 //Article:NewsInHouse 及其依賴 Target 的產(chǎn)物中, swiftmodule 之類的編譯依賴部分。
bg //Article:NewsInHouse applebin_ios-*
這一條 output group 對(duì)應(yīng)的是 //Article:NewsInHouse 及其依賴 Target 的輸入文件中的非源文件的部分,比如編譯時(shí)依賴的 hmap 。
這樣的 Bazel Build 命令可以在 Index Build 時(shí)將 hmap 和 swiftmodule 之類的索引中間產(chǎn)物生成出來,然后再索引具體文件時(shí)就不會(huì)因?yàn)槿鄙龠@些中間產(chǎn)物而失敗。
并且這里的依賴關(guān)系是由 Bazel 去處理的,不是必須像 Xcode 原生機(jī)制那樣,按照依賴關(guān)系來決定 Index Build 中 Target 的順序。
所以僅從 "Module 中間產(chǎn)物" 這方面來說,Xcode 中的依賴關(guān)系并不是必需的。
多 Target 編譯參數(shù)
除了中間產(chǎn)物生成之外,依賴關(guān)系在多個(gè) Target 共用源文件時(shí)的編譯參數(shù)計(jì)算也發(fā)揮著作用。這里的“編譯參數(shù)計(jì)算”是指當(dāng)一個(gè)源文件被不同 Target 引用時(shí),應(yīng)用的編譯參數(shù)可能不同的情況。這么介紹比較抽象,來看下具體的例子:
下面 Demo 工程中有兩個(gè) App Target:AppA 和 AppB
- 在兩個(gè) App Target 的 Build Settings 中分別注入了宏
IS_APP_A
和IS_APP_B
。
- 有一份公共的代碼文件 public.m 分別被添加到兩個(gè) App Target 的 Compile Sources 中。
- public.m 內(nèi)用預(yù)編譯宏隔離了存在差異的邏輯
- 隨著我們切換構(gòu)建的 App Scheme,由于編譯參數(shù)的差異,宏作用域中高亮的代碼區(qū)域也會(huì)隨之變化(如下圖)。
此時(shí)的工程結(jié)構(gòu)如下圖所示,Xcode 可以通過選中的 AppA Scheme 獲取到 AppA Target 的 Build Settings(圖中紅線路徑),正確地傳遞編譯參數(shù)-DIS_APP_A=1
。
實(shí)際的情況會(huì)復(fù)雜一些,因?yàn)楣こ痰慕M件化建設(shè)將代碼下沉到了一個(gè)個(gè)組件內(nèi),而非直接與 App Target 關(guān)聯(lián)。此時(shí)同一份代碼文件在不同 App Target 內(nèi)的索引參數(shù)計(jì)算,則是通過 Target 之間的依賴關(guān)系實(shí)現(xiàn)的。
對(duì)應(yīng)到 Xcode 中,Xcode 可以通過 App Target -> Library Target 的依賴關(guān)系,應(yīng)用對(duì)應(yīng)的 Build Settings 生成索引。
此時(shí)的工程結(jié)構(gòu)則變成了下圖的模式,Xcode 依然可以通過 AppA Target 與 PublicLibraryA Target 的依賴關(guān)系應(yīng)用正確的編譯參數(shù)(圖中紅線路徑)。
Xcode 原生工程和 Tulsi 生成的 Xcode 工程都是通過這種依賴關(guān)系來保證編譯參數(shù)的正確計(jì)算的。
而 rules_xcodeproj 生成的工程完全移除了 Target 之間的依賴關(guān)系,轉(zhuǎn)而給所有 Target 都添加了對(duì) BazelDependencies 的依賴(如下圖所示)。
從圖中可以看到,在缺少 AppA Target 對(duì) PublicLibraryA Target 依賴的情況下,對(duì)于同時(shí)被 PublicLibraryA 和 PublicLibraryB 引用的 public.m ,Xcode 無法感知應(yīng)該應(yīng)用哪個(gè) Target 中的編譯參數(shù)(圖中紅線路徑無法關(guān)聯(lián) AppA Scheme 與 public.m)。此時(shí) Xcode 觸發(fā)索引時(shí)應(yīng)用的 Build Settings 是固定的,不會(huì)隨著構(gòu)建 App Scheme 切換而更改。
具體的表現(xiàn)當(dāng)構(gòu)建目標(biāo)從 AppB 切換到 AppA 時(shí),IS_APP_B
宏中的代碼仍然會(huì)展示為高亮,而不會(huì)隨之切換,從而給開發(fā)者帶來困惑。
對(duì)于這個(gè)問題,rules_xcodeproj 可以通過構(gòu)建索引解決,因?yàn)橐蕾囆畔⒃?Bazel 側(cè)(BUILD 文件中)是完整的,所以觸發(fā)構(gòu)建后可以讓代碼高亮正確展示。
但由于編輯索引使用的參數(shù)是 Xcode 從文件所屬的 Library Target 的 Build Settings 中獲取的,因此在代碼編輯過程中仍然會(huì)出現(xiàn)高亮錯(cuò)誤的問題。
構(gòu)建索引:指在構(gòu)建過程中輸出索引產(chǎn)物,需要通過 index-import 導(dǎo)入 Xcode 緩存目錄(Derived Data)下供 Xcode 消費(fèi)。
編輯索引:在代碼編輯過程中實(shí)時(shí)生成的索引,在內(nèi)存中消費(fèi)索引結(jié)果,不會(huì)將產(chǎn)物寫入磁盤。
在這個(gè)場(chǎng)景下,rules_xcodeproj 移除依賴的做法是有缺陷的。
那么我們要在 rules_xcodeproj 恢復(fù) Target 間的依賴關(guān)系么?
答案是不需要。首先,前文有提及大量復(fù)雜的依賴關(guān)系會(huì)導(dǎo)致 Xcode 卡頓,不應(yīng)恢復(fù);其次,要解決這類代碼高亮錯(cuò)誤的問題,需要的其實(shí)并不是所有 Target 之間的依賴關(guān)系,而是源碼文件與當(dāng)前構(gòu)建 App Target 的關(guān)系。
回顧一下 Demo 工程最簡(jiǎn)單的結(jié)構(gòu),當(dāng)源碼文件直接被對(duì)應(yīng) App Target 引用時(shí),是不需要 Library Target 間的依賴關(guān)系來建立聯(lián)系的。
基于這一思路,我們將所有 Library Target 的源碼合并到了對(duì)應(yīng) App Target。當(dāng)然,直接合并源碼以后索引并不能正常工作,需要對(duì)受影響的功能點(diǎn)進(jìn)行適配,這些適配將在下一節(jié)源碼合并方案中展開介紹。
源碼合并方案
索引參數(shù)接管
將所有源碼合并至 App Target 雖然能解決文件與 App 的關(guān)聯(lián)問題,但各個(gè) Library Target 編譯參數(shù)是不同的,聚合之后不同 Target 下源文件的參數(shù)就無法通過 Build Settings 區(qū)分了。
對(duì)于這個(gè)問題,我們是通過 XCBBuildServiceProxy 接管索引參數(shù)計(jì)算解決的。
索引構(gòu)建時(shí),Xcode 會(huì)先將文件所屬 Target 的 Build Settings 發(fā)送給 XCBBuildService 處理成編譯器理解的參數(shù),再交給 SKAgent 觸發(fā)編譯器進(jìn)行實(shí)際編譯行為。而我們?cè)?XCBBuildServiceProxy 的基礎(chǔ)上實(shí)現(xiàn)了 BitSkyXCBBuildService,可以攔截 Xcode 發(fā)給 XCBBuildService 的請(qǐng)求,通過 Bazel aquery 查詢到具體文件的編譯參數(shù),直接返回給 Xcode 完成后續(xù)的索引構(gòu)建行為。
完成源碼合并與索引參數(shù)接管這兩步改造以后,工程結(jié)構(gòu)如下圖所示??梢钥吹?AppA Scheme 能夠直接通過 AppA Target 關(guān)聯(lián)到 public.m(圖中紅線),從而正確地應(yīng)用編譯參數(shù),高亮對(duì)應(yīng)代碼塊。rules_xcodeproj 移除依賴關(guān)系的副作用也完全被修復(fù)。
Library Target 移除
完成源碼合并以及索引參數(shù)的接管之后,Library Target 中的主要信息( Build Settings 和源碼)都不再有意義了,是否能將這些 Target 信息直接移除呢?
經(jīng)過梳理,Library Target 主要有以下三個(gè)作用,在完成源碼合并以及索引參數(shù)接管后,僅需對(duì)“Module 中間產(chǎn)物生成”進(jìn)行一些改造即可將幾百個(gè) Library Target 的信息進(jìn)行移除,大幅精簡(jiǎn)工程文件的內(nèi)容。
Library Target 作用 | 說明 | 適配方案 |
觸發(fā) Xcode 索引 | Xcode 只會(huì)對(duì)添加到 Build Phase - Compile Sources 中的源碼文件生成索引 | 將源碼添加到 App Target 的 Compile Sources 中有相同的效果; |
按 Target 維度隔離 Build Settings | Xcode 原生的索引功能會(huì)通過 Build Settings 生成文件的編譯參數(shù) | 通過 XCBBuildServiceProxy 接管索引參數(shù)請(qǐng)求,交由 Bazel aquery 查詢具體文件的編譯參數(shù) |
Module 中間產(chǎn)物生成 | 在 Library Target 的 Build Phase 觸發(fā)各個(gè) Target 維度的中間產(chǎn)物生成 | 將所需產(chǎn)物聚合到各個(gè) App Target 的 Build Phase 觸發(fā)生成 |
最終的工程結(jié)構(gòu)如下圖所示。
整體方案上線后,頭條工程文件(project.pbxproj)行數(shù)從 45w 減少至 35w,工程啟動(dòng)與文件操作耗時(shí)也比原來的 Tulsi 工程減少了 60% 以上。
Tulsi | rules_xcodeproj(原生) | rules_xcodeproj(源碼合并) | |
工程首次冷啟 | 47s | 22s | 16s |
二次啟動(dòng) | 33s | 16s | 12s |
文件操作 | 新增 20s 刪除 23s | 新增 13s 刪除 11s | 新增 8s 刪除 6s |
p.s. 源碼合并后存在一個(gè)副作用是 Xcode Build Phase 頁面加載時(shí)間會(huì)增加很多,但考慮到使用 bazel 構(gòu)建后我們并不需要在 Build Phase 修改配置,這個(gè)副作用是可以接受的。
構(gòu)建
rules_xcodeproj 目前提供了兩種 Build 模式,分別是 "Build with Xcode" 和 "Build with Bazel"。
- "Build with Xcode" 模式下,構(gòu)建行為是由 Xcode 接管的。
- "Build with Bazel" 模式下,構(gòu)建行為是由 Bazel 接管的。
據(jù) rules_xcodeproj 官方介紹,"Build with Xcode" 模式在 Bazel 7 下將很難支持,并且即將到來的新的增量生成模式也會(huì)放棄 "Build with Xcode" 。
所以這里主要看一下 "Build with Bazel" ,這個(gè)模式生成的工程中,宿主 Target 對(duì)應(yīng)一個(gè) XCScheme,這個(gè) Scheme 的 Build Pre-actions 里生成一個(gè) SCHEME_Target_IDS_FILE 用于記錄 Target 的 Bazel Label。然后宿主 Target 依賴了 Target BazelDependencies,Xcode 在構(gòu)建宿主 Target 之前會(huì)先構(gòu)建 BazelDependencies。BazelDependencies 通過 Build Phase 去調(diào)用 Bazel Build,這個(gè)時(shí)候會(huì)解析 SCHEME_Target_IDS_FILE 獲取需要構(gòu)建的 Bazel Target。BazelDependencies 構(gòu)建完成后,宿主 Target 的 Build Phase 里會(huì)去把相應(yīng)的 Bazel 的產(chǎn)物拷貝到 Xcode Derivedata 目錄下。
另外,在 rules_xcodeproj 的規(guī)劃中,未來還會(huì)提供一種新的模式,叫做 "Build with Proxy",在這個(gè)模式下,會(huì)通過 XCBBuildServiceProxy 完全繞過 Xcode build system,由 Bazel 控制整個(gè) build 過程。相比 "Build with Bazel" ,這個(gè)模式可以帶來一些更貼近原生的 Xcode 使用體驗(yàn),比如:
- 無需添加
BazelDependencies
Target - 可以去掉重復(fù)的 warnings/errors
- 可以有更穩(wěn)定的索引效果
- 可以在進(jìn)度條展示更多信息
- 可以有更詳細(xì)的 Build 報(bào)告
當(dāng)然這種模式也存在比較大的問題
- 在不同 Xcode 版本之間,Xcode 和 XCBBuildService 交互的 API 可能會(huì)有一些破壞性的變更,需要逐一適配
- 需要在 Xcode 啟動(dòng)時(shí)注入環(huán)境變量,將 XCBBuildService 指向自定義的 XCBBuildServiceProxy
BitSky 目前采用的方案和 "Build with Proxy" 是類似的,通過 BitSkyXCBBuildService 接管 Xcode 的 build 行為。在用戶點(diǎn)擊 Build 時(shí),BitSkyXCBBuildService 里可以從宿主 Target 的 Build Settings 里解析獲取對(duì)應(yīng)的 Bazel Target,然后再由 BitSky 生成調(diào)用 Bazel Build 的命令,這樣可以保證 Bazel Build 的參數(shù)完全由 BitSky 控制,同時(shí)可以通過 Bazel 的 Build Event Protocol 來更好的提供 Xcode 的進(jìn)度 和 Build 日志展示。
同時(shí)為了保證在打開生成的 Xcode 工程時(shí),都能夠使用 BitSkyXCBBuildService,BitSky 在生成工程同時(shí),會(huì)生成一個(gè) Xcode 的影子分身 BitSkyXcode。使用這個(gè) BitSkyXcode 打開工程,無需手動(dòng)注入環(huán)境變量,體驗(yàn)上和使用原生 Xcode 打開工程基本一致。
總結(jié)
本文主要介紹了我們將 Xcode 工程生成工具切換到 rules_xcodeproj 過程中做的一些適配和優(yōu)化工作:
- 索引方面:
- 在分析 Tulsi 與 rules_xcodeproj 工程文件的過程中我們注意到最大的差異在于 rules_xcodeproj 移除了 Library Target 間的依賴關(guān)系,這也是 Tulsi 工程更加卡頓的罪魁禍?zhǔn)住?/li>
- rules_xcodeproj 移除依賴關(guān)系后會(huì)導(dǎo)致多 Target 共用的源文件語法高亮異常,我們通過源碼合并方案解決了這個(gè)問題,并且精簡(jiǎn)了工程文件信息,提升了 Xcode 流暢度。
- 構(gòu)建方面:
我們通過 BitSkyXCBBuildService 接管了 Xcode 的 build 行為,能夠更好地管理構(gòu)建參數(shù)并在 Xcode 提供構(gòu)建進(jìn)度和日志的展示。
在完成切換之后,雖然 Xcode 代碼編輯過程中的卡頓得到了明顯的緩解,但本地研發(fā)的調(diào)試過程,仍然存在 Xcode 卡頓/卡死等現(xiàn)象,對(duì)研發(fā)同學(xué)的開發(fā)工作存在較大困擾。后續(xù),我們將針對(duì)調(diào)試體驗(yàn),從生成工程的角度做一些優(yōu)化工作。
目前考慮基于 Focus Mode 的理念,從底層能力上支持研發(fā)同學(xué)僅關(guān)注與當(dāng)前需求開發(fā)相關(guān)聯(lián)的部分代碼,比如:
- 裁剪 Xcode 工程中需要索引的源代碼;
- 裁剪構(gòu)建過程中需要執(zhí)行編譯源代碼;
- 裁剪調(diào)試時(shí)調(diào)試器需要加載調(diào)試信息;
另外在用戶側(cè),通過策略智能幫助研發(fā)同學(xué),選擇和添加需要 "Focus" 的源碼。
參考文檔
Tulsi (https://github.com/bazelbuild/tulsi)
rules_xcodeproj (https://github.com/MobileNativeFoundation/rules_xcodeproj)
Migrating from Xcode to Bazel (https://bazel.build/migrate/xcode)