軟體設計

[模組化 #5] Xcode 建置變慢的原因與解法(含測量工具)

修改一行程式碼後按下建置,卻等上好幾分鐘;這是每位 iOS 開發者都曾有過的經驗。

閱讀 4 分鐘
[模組化 #5] Xcode 建置變慢的原因與解法(含測量工具) 封面圖

修改一行程式碼後按下建置,卻等上好幾分鐘;這是每位 iOS 開發者都曾有過的經驗。

假設每天建置數十次,每次縮短1分鐘,就能毫不誇張地每天多出30分鐘以上。

先說核心答案:Xcode 建置變慢最大的結構性原因,是即使只改一點,重新編譯的範圍仍然太大;模組化正是縮小這個範圍的解法。

本文將分層整理建置變慢的原因、模組化發揮效果的位置,以及如何用數字測量改善前後的差異。

以下是重點總結。

  1. 增量建置的重新編譯範圍會限制在模組內。沒有模組時,整個 App 就是一個單位。
  2. 不要在沒有測量的情況下調校。Xcode 的 Build With Timing Summary 是基本工具。
  3. 除了模組化,也要檢查型別推導瓶頸、dSYM 設定等局部原因。

為什麼 Xcode 建置會變慢?

原因可以分成三個層次。

第一,重新編譯範圍。

Swift 在單一檔案變更時,會重新編譯該檔案所屬模組中受影響的檔案。問題在於,未模組化的 App 會把整個 Target 視為一個模組。

在數十萬行程式碼的單一 Target 中,微小修改也會造成大範圍重新編譯。尤其修改多處共用的型別時,幾乎就接近完整建置。

第二,型別檢查瓶頸。

Swift 編譯器會花很多時間進行型別推導。一個複雜運算式可能就耗費數秒。冗長鏈式呼叫、複雜三元運算子,以及龐大的 SwiftUI body 都很常見。

第三,建置設定。

如果 Debug 建置仍產生 dSYM(DWARF with dSYM File),或錯誤啟用 Whole Module Optimization,每次建置都會增加不必要的成本。


模組化如何改善建置速度?

模組化改善的是第一個層次:重新編譯範圍。

模組邊界就是重新編譯的防火牆。只要模組內部實作變更,模組外部就不需要重新編譯。

上一篇文章的分層結構在這裡就能發揮作用。

  • 修改搜尋功能(FeatureSearch)內部時,重新編譯只需進行到 FeatureSearch 和 App Target。
  • 相反地,修改所有人都相依的下游模組(CoreNetwork)公開介面時,上游模組會接連重新編譯。

因此,可以直接推導出以建置速度為考量的模組設計原則。

  1. 經常變動的程式碼放在圖的上游(Feature),穩定的程式碼放在下游(Core·Domain)。
  2. 讓下游模組的 public 介面維持最小(不是 public 就不會影響外部)。
  3. 不要建立所有人都相依的肥大 Common 模組。

也有平行化的效果。沒有相依關係的模組會由 Xcode 同時編譯,因此圖越寬、越淺,完整建置也越快。

有模組邊界時,重新編譯會在這個範圍內結束
有模組邊界時,重新編譯會在這個範圍內結束

如何測量建置時間?

憑感覺進行最佳化,大多會失準。三個工具就足夠了。

Build With Timing Summary。 執行 Xcode 選單 Product → Perform Action → Build With Timing Summary 後,建置記錄結尾會整理各階段耗時。先在這裡確認是哪個 Target、哪個階段形成瓶頸。

型別檢查警告旗標。 編譯器會直接指出耗時較久的函式與運算式。將它加入 Other Swift Flags。

-Xfrontend -warn-long-function-bodies=100
-Xfrontend -warn-long-expression-type-checking=100
// 100ms 對超過限制的函式與運算式發出警告

建置時間軸。 在建置記錄的 Assistant 區域開啟時間軸,即可看到各 Target 的平行執行狀況。若出現長時間串行排列的區段,表示相依圖阻礙了平行化。


除了模組化,還要確認哪些事項?

測量後常會發現,問題不是模組化,而是某一行設定。以下是以 Debug 建置為基準的檢查清單。

項目 建議設定(Debug)
Debug Information Format DWARF(關閉 dSYM 產生)
Compilation Mode Incremental
Optimization Level -Onone
Build Active Architecture Only Yes

從 Xcode 16 開始,導入了明確建置模組(Explicitly Built Modules),改善模組準備階段的平行化。使用最新 Xcode 本身也是提升建置速度的方法。

面試時可以這樣問

Q. 請說明在大型 iOS App 中縮短建置時間的方法。

首先用 Build With Timing Summary 測量瓶頸,並檢查設定(Debug 的 dSYM 產生、編譯模式)。在結構上,關鍵是透過模組化將重新編譯範圍限制在模組內,把經常變動的程式碼放在相依圖上游,並將下游模組的 public 表面積降到最低。

Q. 已經拆分模組,建置仍然很慢時,你會懷疑什麼?

確認所有人都相依的下游模組是否經常變動,以及 public 介面是否頻繁修改。再用警告旗標找出型別檢查瓶頸、拆分複雜運算式,並透過建置時間軸確認相依圖是否被串行化而阻礙平行編譯。

每天累積等待建置的時間,會比想像中更多
每天累積等待建置的時間,會比想像中更多

建置速度是模組化最有感的回報,但模組增加到數十個後,專案管理本身又會變成一項工作。

系列最後一篇將介紹能降低這項管理成本的工具 Tuist,也會整理擺脫 pbxproj 衝突與二進位快取的方法。