如何提高 Xcode 的編譯速度

Joy_xx發表於2018-06-14

本文總結自 WWDC 2018 building faster in xcode

該 Session 通過一系列的實踐來實現 Xcode 的快速編譯,共闡述了六個大方面,分別是:

  • 將編譯過程並行化
  • 通過指定輸入、輸出檔案減少指令碼的重複編譯
  • 測量你的編譯時間,找到優化的突破點
  • 理解 Swift 檔案和工程之間的依賴
  • 處理複雜的表示式
  • 減少 Objective-C 和 Swift 暴露的介面

如何提高 Xcode 的編譯速度

編譯並行化

通常,我們的 Target 都會顯式依賴其他 Target,在連結的時候會隱式連結其他很多庫(Library)。以一個遊戲的依賴為例,Tests Target 會依賴 Game、Shaders、Utilities,同時 Game 也需要依賴 Shaders、Utilities、Physics。

如何提高 Xcode 的編譯速度

如果他們的 build 順序是按照序列順序,那麼他們的構建順序和時間如下,他們之間需要等待前一個 build 完成後才可以繼續,是非常耗時的,浪費了工程師們太多的時間。

如何提高 Xcode 的編譯速度

如果採用並行 build,則會節省大量的時間,效果如下圖所示。此次 build 過程並沒有減少工作量,但是時間卻減少了很多。

如何提高 Xcode 的編譯速度

那麼如何實現並行 build 呢?可以在 Xcode 中進行配置完成。點選 Target,然後點選 Edit Scheme,點選 build 配置,勾選 paralielize Build 和 Find implicit Dependencies 選項。

如何提高 Xcode 的編譯速度

上面的序列 build 是如何變成並行 build 的效果的呢?以 Test Target 為例,它需要同時測試 Game、Shaders、Utilities 這三個元件,如果序列所需要花費的時間如下圖所示

如何提高 Xcode 的編譯速度

如果把三個元件分開來測試,效果就大不相同了。可以看到紫色的 Test Target的 build 時間提前了很多,這樣 Test Target build 就可以和後續的其他任務並行,節省時間。

如何提高 Xcode 的編譯速度

再者一點就是減少依賴暴露。Shaders target 依賴 Utilities,但是它可能只需要 Utilities 中一小部分程式碼和功能,那麼我們可以進行剝離,這樣一個小的改進將帶來 build 速度大幅提升。可以看到下圖,Code Gen 可以和 Physics 一起進行 build,提高了併發性。

如何提高 Xcode 的編譯速度

測試未使用到的依賴。比如 Utilities 可能完全沒必要依賴 Physics,如果解除他們之間的依賴關係,build 並行圖會有新的變化,Utilities 的時機又可以提前,當 Code Gen build 完成,它就可以開始 build,和 Shaders 幾乎在同一時刻並行 build

如何提高 Xcode 的編譯速度

同時,Xcode 10 優化了 Target 之間的 build 過程,如果 TargetB 依賴 Target A,那麼 TargetB 不需要 TargetA 完全 build 完成,就可以開始 build了,只要保證 TargetB 所需要 Code build 完成即可,這樣 TargetB 就可以更早的開始 build。但是如果 Target 在 build phases 有配置執行指令碼 ,那麼必須要等待指令碼執行完成才可以。

如何提高 Xcode 的編譯速度

Run Script Phases

在 build phases 中配置執行指令碼可以讓我 Xcode 按照我們的需要定製 build 過程,如下所示,新增的指令碼的時候,可以指定指令碼或者指令碼路徑、輸入檔案、輸出檔案。

如何提高 Xcode 的編譯速度

這個指令碼有幾個固定的執行時機,分別是

  • No input files declared (沒有宣告輸入檔案)
  • Input files changed(輸入檔案發生改變)
  • Output files missing(輸出檔案缺失)

我們應該指定 input files 和 output files,因為如果不指定,Xcode 就會每次增量編譯的時候執行一次這個 build 指令碼,增加了 build 的時間。

如何提高 Xcode 的編譯速度

依賴迴圈是很常見的,Xcode 10 提供了很好用的診斷機制和詳細的文件

如何提高 Xcode 的編譯速度

如何提高 Xcode 的編譯速度

測量編譯的時間

我們可以通過 Xcode 的 log 顯示每個 Target 的編譯時間和連結是多少

如何提高 Xcode 的編譯速度

同時 Xcode 10 提供了一個新的 feature,就是 Timing Summary,可以通過點選 Product -> Perform Action -> Build With Timing Summary 進行編譯,這樣在 Build Log 的末尾就會新增 Timing Summary Log。可以看到第一條就是 Phase 指令碼的執行時間 5.036 秒,我們可以通過這個 log 看到哪個階段是耗時的,便於我們進行優化。

如何提高 Xcode 的編譯速度

Timing Summary 在終端也是可以使用的,只要加上 -showBuildTimingSummary 標記即可

如何提高 Xcode 的編譯速度

原始碼級別的優化

首先講了一個 Xcode 設定的小 Tip,在 Xcode 10 中增強了增量編譯(Incremental)的能力,我們可以設定 Compliation Mode 在 Debug 模式下為 Incremental,這樣雖然全量編譯一次會比模組化編譯(Whole Module)慢,但是之後修改一次檔案就只需要再編譯一次相關的檔案即可,而不必整個模組都重新編譯一次,提高了後續的編譯效率。

處理複雜表示式

為複雜的屬性使用明確的型別

首先來看下面一段程式碼,這個 struct 在專案的很多地方都用到了,這個結構體有一個問題就是它有一點複雜,如果沒有標明明確的型別,那麼編譯器就需要每次用的時候進行型別推斷,並且你同事開發的時候也需要去猜測這個屬性的型別是什麼。如果顯式註明型別則不僅可以提高編譯效率,還體現了優秀工程師的編碼素養。

struct ContrivedExample {
var bigNumber = [4, 3, 2].reduce(1) {
        soFar, next in
        pow(next, soFar) }.
}.

複製程式碼

優化後的效果如下:

struct ContrivedExample {
var bigNumber: Double = [4, 3, 2].reduce(1) {
        soFar, next in
        pow(next, soFar) }.
}.
複製程式碼

明確複雜閉包的型別

推斷 Closures 型別,有時候是方便,但是有時候卻給我們帶來了問題。比如下面這段程式碼除了非常醜陋以外,還會導致 Swfit 編譯器在短時間內無法推斷出該表示式的含義。

func sumNonOptional(i: Int?, j: Int?, k: Int?) -> Int? { 
    return [i, j, k].reduce(0) {.
        soFar, next in
        soFar != nil && next != nil ? soFar! + next! :
            (soFar != nil ? soFar! : (next != nil ? next! : nil)) }.
}.
複製程式碼

編譯器會報錯:

如何提高 Xcode 的編譯速度

受到上個示例的啟示,我腦海中首先想到的就是為 Closure 提供明確的型別,如下所示,但是對於這個例子來說,可能不是那麼必要,sumNonOptional 方法的引數和返回值已經很明確,所以對於 Closure 來說資料型別也是明確的。更好的辦法應該是簡化這個複雜的表示式。

func sumNonOptional(i: Int?, j: Int?, k: Int?) -> Int? { 
    return [i, j, k].reduce(0) {
        (soFar: Int?, next: Int?) -> Int? in
        soFar != nil && next != nil ? soFar! + next! :
            (soFar != nil ? soFar! : (next != nil ? next! : nil)) }.
}.
複製程式碼

拆解複雜表示式

可以將示例 2 中的複雜表示式進行簡化,簡化的程式碼不僅可以提高編譯效率,也具有更好的可讀性。

func sumNonOptional(i: Int?, j: Int?, k: Int?) -> Int? { 
    return [i, j, k].reduce(0) {
        soFar, next in
 
        if let soFar = soFar {
            if let next = next { return soFar + next } 
            return soFar
        } else {
             return next
        }
} }
複製程式碼

謹慎使用 AnyObject 型別的方法和屬性

下面這段程式碼使用了 AnyObject 標示的型別,Swift 中的 AnyObject 和 Objective-C 中的 ID 型別很類似,Swift 中也允許這麼使用,但是這樣做會存在一些問題,當用 delegate 去呼叫 myOperationDidSucceed 方法時,編譯器並不知道具體是呼叫哪個方法,所以編譯器會去工程和依賴的 Framework 中遍歷所有可能的方法,這樣增加了編譯時間。

weak var delegate: AnyObject? 
func reportSuccess() {
    delegate?.myOperationDidSucceed(self) 
}.
複製程式碼

建議的方法是減少 AnyObject 的使用,用明確的型別代替,如下所示。這樣明確了我們想要呼叫的方法來自哪個類,編譯器可以直接進行呼叫,減少了遍歷的時間。

weak var delegate: MyOperationDelegate? 
func reportSuccess() {
    delegate?.myOperationDidSucceed(self) 
}.

protocol MyOperationDelegate: class {
    func myOperationDidSucceed(_ operation: MyOperation)
}
複製程式碼

理解 Swift 檔案和工程依賴

增量編譯是基於檔案的,假如原始依賴如下圖所示

如何提高 Xcode 的編譯速度

此時在左面的檔案的 Struct 內部做修改,Swift 編譯器只會重新編譯器左面的檔案,而不會重新編譯右側的檔案

如何提高 Xcode 的編譯速度

但是如果在左側檔案增加了新的 API,雖然並不會影響右側的檔案正常編譯,但是編譯器是保守的,也會重新進行編譯。

如何提高 Xcode 的編譯速度

在一個 target 內部檔案的更改不會影響其他 target,只需要重新編譯該 target 內部和該檔案有依賴關係的檔案即可。

如果一個檔案在 target 內部有依賴,在其他 target 也需要依賴這個檔案,對於這種跨 target 的情況,該檔案修改,會影響所有的 target ,所有 target 都要進行重新編譯。

如何提高 Xcode 的編譯速度

減少 Objective-C/Swift 暴露的介面

對於一個混編專案來說,Objective-C Bridging Header 是 Objective-C 向 Swift 暴露的介面,Swift 生成的 *-Swift.h 代表的是Swift 向 Objective-C 暴露的介面。

如何提高 Xcode 的編譯速度

對於下面的兩個例子,statusField 屬性、close 方法 和 keyboardWillShow 方法都會在 *-Swift.h 中暴露給 Objective-C,這些屬性和方法可能在 Objective-C 中是完全沒有使用到的,所以這是完全沒有必要的,我們應使用 private 來修飾他們,比如 @IBAction private func close(_ sender: Any?) { ... } ,儘可能減少暴露的介面數量。

class MainViewController: UIViewController { 
    @IBOutlet var statusField: UITextField! 
    @IBAction func close(_ sender: Any?) { ... }
}
複製程式碼
@objc func keyboardWillShow(_: Notification) { 
    // Important keyboard setup code here.
}.
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), ...)
複製程式碼

推薦 block API 來實現上面的通知,程式碼更簡潔,而且不用擔心過多暴露 API 的問題

self.observer = NotificationCenter.default.addObserver( forName: UIKeyboardWillShow, object: nil, queue: nil) {
    // Important keyboard setup code here.
}.
複製程式碼

將 Swift 3.0 升級到最新版本,Xcode10 將是最後相容 Swift 3.0 的版本,對於 Swift 3.0 繼承於 NSObject 的類方法和屬性都會預設加上 @objc,把 API 都暴露給 Objective-C 呼叫(Swift 4 已經廢除了該機制)。應得減少隱式 @objc 自動推斷,在設定中將 Swift3 @objc Inference 修改為 Defalut

如何提高 Xcode 的編譯速度

對於混編專案,減少 Objective-C 的介面暴露也是必要的,比如對於下面這種情況,Bridging-Header 暴露了 myViewController,但是 myViewController內部又引用了其他標頭檔案,可能 networkManager 在 Swift 中並沒有使用到,那麼這樣暴露 myNetworkManager 就完全沒有必要了,可以使用 Category 來隱藏不必要暴露的標頭檔案。

如何提高 Xcode 的編譯速度

優化後的效果如下:

如何提高 Xcode 的編譯速度

參考

相關文章