該 Session 通過一系列的實踐來實現 Xcode 的快速編譯,共闡述了六個大方面,分別是:
- 將編譯過程並行化
- 通過指定輸入、輸出檔案減少指令碼的重複編譯
- 測量你的編譯時間,找到優化的突破點
- 理解 Swift 檔案和工程之間的依賴
- 處理複雜的表示式
- 減少 Objective-C 和 Swift 暴露的介面
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/ceec16eb48f1d638abe13cd4efc499227b776015f2b3a8188977a14d86fad968.png)
編譯並行化
通常,我們的 Target 都會顯式依賴其他 Target,在連結的時候會隱式連結其他很多庫(Library)。以一個遊戲的依賴為例,Tests Target 會依賴 Game、Shaders、Utilities,同時 Game 也需要依賴 Shaders、Utilities、Physics。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/8c8a70d6a6443e5e0c26ad675bcd73a70f1390317f8045acd13dec8b072df500.png)
如果他們的 build 順序是按照序列順序,那麼他們的構建順序和時間如下,他們之間需要等待前一個 build 完成後才可以繼續,是非常耗時的,浪費了工程師們太多的時間。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/0dd83cca616527682e30100803f2994d73254279fa1b3c75d673e13d1f320412.png)
如果採用並行 build,則會節省大量的時間,效果如下圖所示。此次 build 過程並沒有減少工作量,但是時間卻減少了很多。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/d2f728da85db2820d121f15f75b51332415413d9e56f58c985c4fe2b269cac5c.png)
那麼如何實現並行 build 呢?可以在 Xcode 中進行配置完成。點選 Target,然後點選 Edit Scheme,點選 build 配置,勾選 paralielize Build 和 Find implicit Dependencies 選項。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/ce75c33c9fbb8be1cba349df5d314b716d58a27a2055f8cdbd3f09b77770a5f3.png)
上面的序列 build 是如何變成並行 build 的效果的呢?以 Test Target 為例,它需要同時測試 Game、Shaders、Utilities 這三個元件,如果序列所需要花費的時間如下圖所示
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/1f8a474e69c2cacd08cb1bdabcc95a9a73ee96b0e04ebe05945ee2a874414538.png)
如果把三個元件分開來測試,效果就大不相同了。可以看到紫色的 Test Target的 build 時間提前了很多,這樣 Test Target build 就可以和後續的其他任務並行,節省時間。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/64805aeed88227cbc3b634e762b009cf07905a49cbd11044754a04e5e78806a3.png)
再者一點就是減少依賴暴露。Shaders target 依賴 Utilities,但是它可能只需要 Utilities 中一小部分程式碼和功能,那麼我們可以進行剝離,這樣一個小的改進將帶來 build 速度大幅提升。可以看到下圖,Code Gen 可以和 Physics 一起進行 build,提高了併發性。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/8ccd0f70dec679cfd88dfc5aee85a4a7bd8b5f3eb4d85d87ff32be4958003416.png)
測試未使用到的依賴。比如 Utilities 可能完全沒必要依賴 Physics,如果解除他們之間的依賴關係,build 並行圖會有新的變化,Utilities 的時機又可以提前,當 Code Gen build 完成,它就可以開始 build,和 Shaders 幾乎在同一時刻並行 build
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/75a9e5228f83f72ab55f3087efb0f8725f944fae430a23366d80a0710d571cfc.png)
同時,Xcode 10 優化了 Target 之間的 build 過程,如果 TargetB 依賴 Target A,那麼 TargetB 不需要 TargetA 完全 build 完成,就可以開始 build了,只要保證 TargetB 所需要 Code build 完成即可,這樣 TargetB 就可以更早的開始 build。但是如果 Target 在 build phases 有配置執行指令碼 ,那麼必須要等待指令碼執行完成才可以。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/9bbdb65458f28477eeff56d52036aaaa75fad766b1e1fce6513f08a01acac3ba.png)
Run Script Phases
在 build phases 中配置執行指令碼可以讓我 Xcode 按照我們的需要定製 build 過程,如下所示,新增的指令碼的時候,可以指定指令碼或者指令碼路徑、輸入檔案、輸出檔案。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/01626847cadb59cacce12b5671a365177895608f9e62f3e4ec3e90894667d8d4.png)
這個指令碼有幾個固定的執行時機,分別是
- No input files declared (沒有宣告輸入檔案)
- Input files changed(輸入檔案發生改變)
- Output files missing(輸出檔案缺失)
我們應該指定 input files 和 output files,因為如果不指定,Xcode 就會每次增量編譯的時候執行一次這個 build 指令碼,增加了 build 的時間。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/706fcd479cce02f186be566ce077f0740146c1a4414f588b697aac929a966c2b.png)
依賴迴圈是很常見的,Xcode 10 提供了很好用的診斷機制和詳細的文件
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/f8b541efe1087384f0bba6d898debcb66eec7e9f41f6de3a22c06f51639f1082.png)
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/22f2b54b80f813a72ebed16234068c1694a203af6a515be1dabc1b2d18158a54.png)
測量編譯的時間
我們可以通過 Xcode 的 log 顯示每個 Target 的編譯時間和連結是多少
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/7da348710e8cd8da6544e04b311a8011841cc107b7ea054945a0f7910d45b357.png)
同時 Xcode 10 提供了一個新的 feature,就是 Timing Summary,可以通過點選 Product -> Perform Action -> Build With Timing Summary 進行編譯,這樣在 Build Log 的末尾就會新增 Timing Summary Log。可以看到第一條就是 Phase 指令碼的執行時間 5.036 秒,我們可以通過這個 log 看到哪個階段是耗時的,便於我們進行優化。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/2cae9fdbfe0b5536c4271862ed559c6ad46b51d2ac565d3db4714e71fb749a2c.png)
Timing Summary 在終端也是可以使用的,只要加上 -showBuildTimingSummary
標記即可
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/7d9581ac95b22eea160b881cc676e3282e450ed90a06d883078103b114878235.png)
原始碼級別的優化
首先講了一個 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 的編譯速度](https://i.iter01.com/images/bc909dcbee329c24e1f1e38b22a070fedc5bc613d8496528dab2e82989e8adc2.png)
受到上個示例的啟示,我腦海中首先想到的就是為 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 的編譯速度](https://i.iter01.com/images/c0b4ff64e19c935c7194936cdaf4e874d8e22f52e5c82549a6d02c1a30c26760.png)
此時在左面的檔案的 Struct 內部做修改,Swift 編譯器只會重新編譯器左面的檔案,而不會重新編譯右側的檔案
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/d4791a3b3ce4a2ccb07bd19b63311d5df859c3ad33c9a5c90144547fc641a428.png)
但是如果在左側檔案增加了新的 API,雖然並不會影響右側的檔案正常編譯,但是編譯器是保守的,也會重新進行編譯。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/95d8a63fa9f2d33012642f4123f0786110d23ace3172c3e90ddc941fe798f929.png)
在一個 target 內部檔案的更改不會影響其他 target,只需要重新編譯該 target 內部和該檔案有依賴關係的檔案即可。
如果一個檔案在 target 內部有依賴,在其他 target 也需要依賴這個檔案,對於這種跨 target 的情況,該檔案修改,會影響所有的 target ,所有 target 都要進行重新編譯。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/f84d9c8ecb29fd7285ffaf25b841f4d51ba0d3258939d9b264ac0deaa517b98f.png)
減少 Objective-C/Swift 暴露的介面
對於一個混編專案來說,Objective-C Bridging Header 是 Objective-C 向 Swift 暴露的介面,Swift 生成的 *-Swift.h
代表的是Swift 向 Objective-C 暴露的介面。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/aa82a254216b536b75d1301337f272e7118855568e55d39dbca5c7d3073a0db4.png)
對於下面的兩個例子,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 的編譯速度](https://i.iter01.com/images/aa483f301bc27ceb948981494dd2cc1030576f9b038dc6f7ee2675c3174080c0.png)
對於混編專案,減少 Objective-C 的介面暴露也是必要的,比如對於下面這種情況,Bridging-Header 暴露了 myViewController,但是 myViewController內部又引用了其他標頭檔案,可能 networkManager 在 Swift 中並沒有使用到,那麼這樣暴露 myNetworkManager 就完全沒有必要了,可以使用 Category 來隱藏不必要暴露的標頭檔案。
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/86c073bf04f7edbe639de254aa9868dae868a9fce8eb12fce09258ddab7cd9d1.png)
優化後的效果如下:
![如何提高 Xcode 的編譯速度](https://i.iter01.com/images/8283ff4ce5ab63fd1793918cbf1e85d70f256d8471debf9bc47fce87a6129639.png)