找出編譯耗時過長的文件
要優(yōu)化項(xiàng)目的編譯速度,首先需要把耗時過長的文件找出來,然后進(jìn)行重點(diǎn)優(yōu)化。這里會用到Xcode build的兩個OTHER_SWIFT_FLAGS:
- -Xfrontend: 如果編譯或類型檢查時耗時多長,則在Xcode中輸出警告。
- -debug-time-function-bodies:輸出每個函數(shù)的編譯時長。
添加這些flag的方法為:
- 選中Target
- 選中Build Settings
- 搜索“Other Swift Flags”
- 添加”-Xfrontend -debug-time-function-bodies“

基于這兩個flag,有3個方法可以找到耗時過長的文件,這三個方法的本質(zhì)是一樣,只不過是手動操作的多少而已,推薦使用第一種。
1.(推薦)使用BuildTimeAnalyzer
BuildTimeAnalyzer是一個macOS app,用于輔助分析Xcode的編譯log。
從github下載這個app的源碼后,在Xcode打開項(xiàng)目,點(diǎn)擊Product >> Archive >> Export,就可以生成BuildTimeAnalyzer。雙擊運(yùn)行看到如下的頁面,

按照使用說明一步步操作之后,就可以看到類似于下圖的界面,點(diǎn)擊任意一行,Xcode就會跳轉(zhuǎn)到對應(yīng)的函數(shù)中。

2. 命令行方式
命令行方式不需要修改Xcode的項(xiàng)目設(shè)置,將下面命令行中的SwiftCompileTime替換為你的項(xiàng)目名,在項(xiàng)目所在目錄下直接運(yùn)行即可,它會將每個文件的編譯時間都輸出到culprits.txt中。這個命令執(zhí)行的時間可能會非常長!一定要耐心等待。
項(xiàng)目后綴名為.xcworkspace時使用:
xcodebuild -workspace SwiftCompileTime.xcworkspace -scheme SwiftCompileTime clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep .[0-9]ms | grep -v ^0.[0-9]ms | sort -nr > culprits.txt
項(xiàng)目后綴名為.xcodeproj時使用:
xcodebuild -project SwiftCompileTime.xcodeproj -scheme SwiftCompileTime clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep .[0-9]ms | grep -v ^0.[0-9]ms | sort -nr > culprits.txt
輸出文件的內(nèi)容的樣式如下:
210.61ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/String+TVGuor.swift:34:34 get {}
196.79ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/String+TVGuor.swift:34:34 get {}
128.94ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/Optional+Extension.swift:23:28 get {}
123.50ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/Optional+Extension.swift:23:28 get {}
119.40ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/VideoAlbum.swift:565:17 @objc public dynamic func formatVideoCount(by type: TVGVideoAlubmType) -> String
117.80ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/String+Extension.swift:109:23 get {}
114.67ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/NSObject+Extension.swift:13:32 get {}
112.09ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/String+Extension.swift:109:23 get {}
110.23ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/NSObject+Extension.swift:13:32 get {}
108.80ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/Constants+Enum.swift:61:17 static func channelNo(fromChannelString channel: String?) -> Int?
103.16ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/String+Extension.swift:26:10 func urlQueryEncoded() -> String
102.63ms /Users/xxx/Documents/Workspace/SwiftCompileTime/SwiftCompileTime/TVGuor/Constants+Enum.swift:61:17 static func channelNo(fromChannelString channel: String?) ->
3.(不推薦)手動查看Xcode log方式
這個方式需要大量的手動操作,效率很低,不推薦,列出來僅做參考。步驟如下:
- 按照上面的教程,在Xcode中添加”-Xfrontend -debug-time-function-bodies“的flags
- 編譯
- 點(diǎn)擊?-9(Xcode 8上是?-8)跳轉(zhuǎn)到Build Report
- 右擊build log >> Expand All Transcripts 就可以看到編譯時長的log了


通過修改項(xiàng)目配置實(shí)現(xiàn)優(yōu)化
優(yōu)先級:高
在修改代碼之前,可以先通過修改Xcode的配置實(shí)現(xiàn)編譯速度的優(yōu)化
- Optimization Level。
- Build Settings -> Optimization Level -> Debug模式下設(shè)置為None。
- Build Settings -> User-Defined -> 添加 SWIFT_WHOLE_MODULE_OPTIMIZATION = YES

- Debug模式下不生成dSYM文件。
- dSYM(debug symbols file)是一個存儲debug信息的文件,每次編譯時都會生成。但在Debu模式下我們并不需要它,所以可以將Debug的值設(shè)為"DWARF",僅在Release使用"DWARF with dSYM File".

避免使用簡潔表達(dá)式
這是一個讓人很糾結(jié)的優(yōu)化。Swift提供了很多簡潔的表達(dá)方式,比如定義變量時可以不用聲明變量的類型,使用??為optional提供默認(rèn)值等,這些功能為Swift開發(fā)者提供了不少便利,也構(gòu)成了Swift簡潔的編程風(fēng)格。不過它們在提供便利的同時,也增加了不少編譯的時間。如何抉擇,是令開發(fā)者頭疼的一個問題。我個人的觀點(diǎn)是,對于那些并沒有提供太多便利的表達(dá)式,一定要替換為編譯更快的寫法;其他簡潔表達(dá)式,只要沒有顯著的增加編譯時長,繼續(xù)使用。
一定要避免使用String1 + String2
優(yōu)先級:高
一定要避免使用加好將兩個字符串合并起來,可以改用"\(string1)\(string2)"。
將
"123" + string1 + "456"
替換為
"123\(string1)456"
一定要避免使用Array1 + Array2
優(yōu)先級:高
一定要避免使用加好將兩個數(shù)組合并起來,可以改用array1.append(contentsOf: array2)。
將
array1 + array2
替換為
array1.append(contentsOf: array2)
對于復(fù)雜變量的定義,添加類型聲明
優(yōu)先級:高
在Swift中定義變量時可以不帶類型聲明,編譯器會根據(jù)初始化的值去猜測變量類型,比如let str = "123"就定義了一個String。這為Swift開發(fā)者提供了很大的便利,但編譯器分析變量類型是需要時間的。從減少編譯時間的角度考慮,肯定是要給每個變量定義都加上類型聲明,但這樣就完全不是在寫Swift的代碼了!
其實(shí)對于簡單的變量定義,比如let str = "123"和let str: String = "123",添不添加類型聲明,編譯時間都差不多,但對于下面兩個復(fù)雜的類型,最好還是加上type。
let myCompany = [
"employees": [
"employee 1": ["attribute": "value"],
"employee 2": ["attribute": "value"],
"employee 3": ["attribute": "value"],
"employee 4": ["attribute": "value"],
"employee 5": ["attribute": "value"],
"employee 6": ["attribute": "value"],
"employee 7": ["attribute": "value"],
"employee 8": ["attribute": "value"],
"employee 9": ["attribute": "value"],
"employee 10": ["attribute": "value"],
"employee 11": ["attribute": "value"],
"employee 12": ["attribute": "value"],
"employee 13": ["attribute": "value"],
"employee 14": ["attribute": "value"],
"employee 15": ["attribute": "value"],
"employee 16": ["attribute": "value"],
"employee 17": ["attribute": "value"],
"employee 18": ["attribute": "value"],
"employee 19": ["attribute": "value"],
"employee 20": ["attribute": "value"],
]
]
// build time: 330ms
let tm1 = ceil(abs(PlayerWaitingStartInterval - timer.fireDate.timeIntervalSinceNow) * 1000)
// build time: 79ms
// Add type annotation
let tm1: Double = ceil(abs(PlayerWaitingStartInterval - timer.fireDate.timeIntervalSinceNow) * 1000)
// build time: 26ms
// Add type annotation & separate into two parts
let interval: Double = abs(PlayerWaitingStartInterval - timer.fireDate.timeIntervalSinceNow)
let tm1: Double = ceil(interval * 1000)
避免使用Nil-Coalescing operator ??
優(yōu)先級:低
??同樣會增加編譯時長。但我覺得這個優(yōu)化的優(yōu)先級并不高,僅在編譯時長真的非常長時才考慮使用。
將
let name = string ?? ""
替換為
if let name = string {
/* string has value */
} else {
/* string is nil*/
}
避免使用條件運(yùn)算符?:
優(yōu)先級:低
?:與??一樣會增加編譯時長,但優(yōu)化的優(yōu)先級也不高。
將
let letter = isFirst ? "a" : "b"
替換為
var letter = ""
if isFirst {
letter = "a"
} else {
letter = "b"
}
預(yù)計(jì)算
優(yōu)先級:中
不要直接在if-else的condition中做計(jì)算,會大大的增加編譯時長??梢韵仍谕獠縿?chuàng)建一個變量保存計(jì)算好的值,再將這個變量作為condition。
將
if number == 60 * 60 {}
替換為
let number: Double = 60 * 60
if number == 3600 {
}
Closures and lazy properties
優(yōu)先級:高
Lazy Property和Closures也可能導(dǎo)致編譯時長的增加。下面這個代碼是一個很普通的lazy Property定義,但它卻有可能導(dǎo)致過長的編譯時間。
// Lazy property
private(set) lazy var chartViewColors: [UIColor] = [
self.chartColor,
UIColor(red: 86/255, green: 84/255, blue: 124/255, alpha: 1),
UIColor(red: 80/255, green: 88/255, blue: 92/255, alpha: 1),
UIColor(red: 126/255, green: 191/255, blue: 189/255, alpha: 1),
UIColor(red: 161/255, green: 77/255, blue: 63/255, alpha: 1),
UIColor(red: 235/255, green: 185/255, blue: 120/255, alpha: 1),
UIColor(red: 100/255, green: 126/255, blue: 159/255, alpha: 1),
UIColor(red: 160/255, green: 209/255, blue: 109/255, alpha: 1),
self.backgroundGradientView.upperColor
]
如果將它改為下面的closure后,編譯時間會更長。
private let createChartViewColors = { () -> [UIColor] in
let colors = [
UIColor(red: 86/255, green: 84/255, blue: 124/255, alpha: 1),
UIColor(red: 80/255, green: 88/255, blue: 92/255, alpha: 1),
UIColor(red: 126/255, green: 191/255, blue: 189/255, alpha: 1),
UIColor(red: 161/255, green: 77/255, blue: 63/255, alpha: 1),
UIColor(red: 235/255, green: 185/255, blue: 120/255, alpha: 1),
UIColor(red: 100/255, green: 126/255, blue: 159/255, alpha: 1),
UIColor(red: 160/255, green: 209/255, blue: 109/255, alpha: 1),
]
return colors
}
解決方案是將初始化代碼移到一個單獨(dú)的函數(shù)中。從代碼風(fēng)格的角度考慮,將過長的初始化代碼移到單獨(dú)的函數(shù)中也是一個好習(xí)慣,查看代碼時可以更加清晰地看出一個class定義了哪些property。
// Cumulative build time: 56.3ms
private(set) lazy var chartViewColors: [UIColor] = self.createChartViewColors()
// Build time: 6.2ms
private func createChartViewColors() -> [UIColor] {
return [
chartColor,
UIColor(red: 86/255, green: 84/255, blue: 124/255, alpha: 1),
UIColor(red: 80/255, green: 88/255, blue: 92/255, alpha: 1),
UIColor(red: 126/255, green: 191/255, blue: 189/255, alpha: 1),
UIColor(red: 161/255, green: 77/255, blue: 63/255, alpha: 1),
UIColor(red: 235/255, green: 185/255, blue: 120/255, alpha: 1),
UIColor(red: 100/255, green: 126/255, blue: 159/255, alpha: 1),
UIColor(red: 160/255, green: 209/255, blue: 109/255, alpha: 1),
backgroundGradientView.upperColor
]
}
奇怪的優(yōu)化
這里列出的是Swift編譯優(yōu)化中的一些奇怪現(xiàn)象,感覺更像是編譯器的一些bug,不必花太多時間在這方便。
CGFloat轉(zhuǎn)化為CGFloat
優(yōu)先級:低
下面的代碼,將一個值為CGFloat的轉(zhuǎn)化為CGFloat時反而會導(dǎo)致編譯時長的大大增加。這是一個很奇怪的現(xiàn)象,估計(jì)是編譯器的bug。
// Build time: 3431.7ms
return CGFloat(M_PI) * (CGFloat((hour + hourDelta + CGFloat(minute + minuteDelta) / 60) * 5) - 15) * unit / 180
// Build time: 3.0ms
return CGFloat(M_PI) * ((hour + hourDelta + (minute + minuteDelta) / 60) * 5 - 15) * unit / 180
Round()
優(yōu)先級:低
下面的代碼只是調(diào)用了round()就增加了97%的編譯時長,很奇怪。
// Build time: 1433.7ms
let expansion = a — b — c + round(d * 0.66) + e
// Build time: 34.7ms
let expansion = a — b — c + d * 0.66 + e
References
Profiling your Swift compilation times
Uber: Swift with a hundred engineers
Regarding Swift build time optimizations