如何提升 Xcode 的編譯速度

本文總結自 WWDC 2018 building faster in xcodexcode

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

  • 將編譯過程並行化
  • 經過指定輸入、輸出文件減小腳本的重複編譯
  • 測量你的編譯時間,找到優化的突破點
  • 理解 Swift 文件和工程之間的依賴
  • 處理複雜的表達式
  • 減小 Objective-C 和 Swift 暴露的接口

編譯並行化

一般,咱們的 Target 都會顯式依賴其餘 Target,在連接的時候會隱式連接其餘不少庫(Library)。以一個遊戲的依賴爲例,Tests Target 會依賴 Game、Shaders、Utilities,同時 Game 也須要依賴 Shaders、Utilities、Physics。閉包

若是他們的 build 順序是按照串行順序,那麼他們的構建順序和時間以下,他們之間須要等待前一個 build 完成後才能夠繼續,是很是耗時的,浪費了工程師們太多的時間。併發

若是採用並行 build,則會節省大量的時間,效果以下圖所示。這次 build 過程並無減小工做量,可是時間卻減小了不少。app

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

上面的串行 build 是如何變成並行 build 的效果的呢?以 Test Target 爲例,它須要同時測試 Game、Shaders、Utilities 這三個組件,若是串行所須要花費的時間以下圖所示模塊化

若是把三個組件分開來測試,效果就大不相同了。能夠看到紫色的 Test Target的 build 時間提早了不少,這樣 Test Target build 就能夠和後續的其餘任務並行,節省時間。測試

再者一點就是減小依賴暴露。Shaders target 依賴 Utilities,可是它可能只須要 Utilities 中一小部分代碼和功能,那麼咱們能夠進行剝離,這樣一個小的改進將帶來 build 速度大幅提高。能夠看到下圖,Code Gen 能夠和 Physics 一塊兒進行 build,提升了併發性。優化

測試未使用到的依賴。好比 Utilities 可能徹底不必依賴 Physics,若是解除他們之間的依賴關係,build 並行圖會有新的變化,Utilities 的時機又能夠提早,當 Code Gen build 完成,它就能夠開始 build,和 Shaders 幾乎在同一時刻並行 buildui

同時,Xcode 10 優化了 Target 之間的 build 過程,若是 TargetB 依賴 Target A,那麼 TargetB 不須要 TargetA 徹底 build 完成,就能夠開始 build了,只要保證 TargetB 所須要 Code build 完成便可,這樣 TargetB 就能夠更早的開始 build。可是若是 Target 在 build phases 有配置執行腳本 ,那麼必需要等待腳本執行完成才能夠。

Run Script Phases

在 build phases 中配置執行腳本可讓我 Xcode 按照咱們的須要定製 build 過程,以下所示,添加的腳本的時候,能夠指定腳本或者腳本路徑、輸入文件、輸出文件。

這個腳本有幾個固定的執行時機,分別是

  • No input files declared (沒有聲明輸入文件)
  • Input files changed(輸入文件發生改變)
  • Output files missing(輸出文件缺失)

咱們應該指定 input files 和 output files,由於若是不指定,Xcode 就會每次增量編譯的時候執行一次這個 build 腳本,增長了 build 的時間。

依賴循環是很常見的,Xcode 10 提供了很好用的診斷機制和詳細的文檔

測量編譯的時間

咱們能夠經過 Xcode 的 log 顯示每一個 Target 的編譯時間和連接是多少

同時 Xcode 10 提供了一個新的 feature,就是 Timing Summary,能夠經過點擊 Product -> Perform Action -> Build With Timing Summary 進行編譯,這樣在 Build Log 的末尾就會添加 Timing Summary Log。能夠看到第一條就是 Phase 腳本的執行時間 5.036 秒,咱們能夠經過這個 log 看到哪一個階段是耗時的,便於咱們進行優化。

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

源碼級別的優化

首先講了一個 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)) }.
}.
複製代碼

編譯器會報錯:

受到上個示例的啓示,我腦海中首先想到的就是爲 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 文件和工程依賴

增量編譯是基於文件的,假如原始依賴以下圖所示

此時在左面的文件的 Struct 內部作修改,Swift 編譯器只會從新編譯器左面的文件,而不會從新編譯右側的文件

可是若是在左側文件增長了新的 API,雖然並不會影響右側的文件正常編譯,可是編譯器是保守的,也會從新進行編譯。

在一個 target 內部文件的更改不會影響其餘 target,只須要從新編譯該 target 內部和該文件有依賴關係的文件便可。

若是一個文件在 target 內部有依賴,在其餘 target 也須要依賴這個文件,對於這種跨 target 的狀況,該文件修改,會影響全部的 target ,全部 target 都要進行從新編譯。

減小 Objective-C/Swift 暴露的接口

對於一個混編項目來講,Objective-C Bridging Header 是 Objective-C 向 Swift 暴露的接口,Swift 生成的 *-Swift.h 表明的是Swift 向 Objective-C 暴露的接口。

對於下面的兩個例子,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

對於混編項目,減小 Objective-C 的接口暴露也是必要的,好比對於下面這種狀況,Bridging-Header 暴露了 myViewController,可是 myViewController內部又引用了其餘頭文件,可能 networkManager 在 Swift 中並無使用到,那麼這樣暴露 myNetworkManager 就徹底沒有必要了,可使用 Category 來隱藏沒必要要暴露的頭文件。

優化後的效果以下:

參考

相關文章
相關標籤/搜索