Swift优化 - 优化编译速度

找出编译耗时过长的文件

要优化项目的编译速度,首先需要把耗时过长的文件找出来,然后进行重点优化。这里会用到Xcode build的两个OTHER_SWIFT_FLAGS

  • -Xfrontend: 如果编译或类型检查时耗时多长,则在Xcode中输出警告。
  • -debug-time-function-bodies:输出每个函数的编译时长。

添加这些flag的方法为:

  1. 选中Target
  2. 选中Build Settings
  3. 搜索“Other Swift Flags”
  4. 添加”-Xfrontend -debug-time-function-bodies“
image

基于这两个flag,有3个方法可以找到耗时过长的文件,这三个方法的本质是一样,只不过是手动操作的多少而已,推荐使用第一种。

1.(推荐)使用BuildTimeAnalyzer

BuildTimeAnalyzer是一个macOS app,用于辅助分析Xcode的编译log。

github下载这个app的源码后,在Xcode打开项目,点击Product >> Archive >> Export,就可以生成BuildTimeAnalyzer。双击运行看到如下的页面,

WX20171103-182846@2x.png

按照使用说明一步步操作之后,就可以看到类似于下图的界面,点击任意一行,Xcode就会跳转到对应的函数中。

BuildTimeAnalyzerSample.png

2. 命令行方式

命令行方式不需要修改Xcode的项目设置,将下面命令行中的SwiftCompileTime替换为你的项目名,在项目所在目录下直接运行即可,它会将每个文件的编译时间都输出到culprits.txt中。这个命令执行的时间可能会非常长!一定要耐心等待。

项目后缀名为.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

项目后缀名为.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

输出文件的内容的样式如下:

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
  • 编译
  • 点击⌘-9(Xcode 8上是⌘-8)跳转到Build Report
  • 右击build log >> Expand All Transcripts 就可以看到编译时长的log了
buildGroup.png
buildGroupLog.png

通过修改项目配置实现优化

优先级:高

在修改代码之前,可以先通过修改Xcode的配置实现编译速度的优化

  • Optimization Level。
    • Build Settings -> Optimization Level -> Debug模式下设置为None。
    • Build Settings -> User-Defined -> 添加 SWIFT_WHOLE_MODULE_OPTIMIZATION = YES
1*5z6bV_NHF8D_AALg8omuUg.png
  • Debug模式下不生成dSYM文件。
    • dSYM(debug symbols file)是一个存储debug信息的文件,每次编译时都会生成。但在Debu模式下我们并不需要它,所以可以将Debug的值设为"DWARF",仅在Release使用"DWARF with dSYM File".
1*CMgdT00APYZClkECjbmvew.png

避免使用简洁表达式

这是一个让人很纠结的优化。Swift提供了很多简洁的表达方式,比如定义变量时可以不用声明变量的类型,使用??为optional提供默认值等,这些功能为Swift开发者提供了不少便利,也构成了Swift简洁的编程风格。不过它们在提供便利的同时,也增加了不少编译的时间。如何抉择,是令开发者头疼的一个问题。我个人的观点是,对于那些并没有提供太多便利的表达式,一定要替换为编译更快的写法;其他简洁表达式,只要没有显著的增加编译时长,继续使用。

一定要避免使用String1 + String2

优先级:高

一定要避免使用加好将两个字符串合并起来,可以改用"\(string1)\(string2)"

"123" + string1 + "456"

替换为

"123\(string1)456"

一定要避免使用Array1 + Array2

优先级:高

一定要避免使用加好将两个数组合并起来,可以改用array1.append(contentsOf: array2)

array1 + array2

替换为

array1.append(contentsOf: array2)

对于复杂变量的定义,添加类型声明

优先级:高

在Swift中定义变量时可以不带类型声明,编译器会根据初始化的值去猜测变量类型,比如let str = "123"就定义了一个String。这为Swift开发者提供了很大的便利,但编译器分析变量类型是需要时间的。从减少编译时间的角度考虑,肯定是要给每个变量定义都加上类型声明,但这样就完全不是在写Swift的代码了!

其实对于简单的变量定义,比如let str = "123"let str: String = "123",添不添加类型声明,编译时间都差不多,但对于下面两个复杂的类型,最好还是加上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 ??

优先级:低

??同样会增加编译时长。但我觉得这个优化的优先级并不高,仅在编译时长真的非常长时才考虑使用。

let name = string ?? ""

替换为

if let name = string { 
 /* string has value */
} else {
 /* string is nil*/
}

避免使用条件运算符?:

优先级:低

?:??一样会增加编译时长,但优化的优先级也不高。

let letter = isFirst ? "a" : "b"

替换为

var letter = ""
if isFirst { 
  letter = "a"
} else {
  letter = "b"
}

预计算

优先级:中

不要直接在if-else的condition中做计算,会大大的增加编译时长。可以先在外部创建一个变量保存计算好的值,再将这个变量作为condition。

if number == 60 * 60 {}

替换为

let number: Double = 60 * 60
if number == 3600 {
}

Closures and lazy properties

优先级:高

Lazy Property和Closures也可能导致编译时长的增加。下面这个代码是一个很普通的lazy Property定义,但它却有可能导致过长的编译时间。

// 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
}

解决方案是将初始化代码移到一个单独的函数中。从代码风格的角度考虑,将过长的初始化代码移到单独的函数中也是一个好习惯,查看代码时可以更加清晰地看出一个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
    ]
}

奇怪的优化

这里列出的是Swift编译优化中的一些奇怪现象,感觉更像是编译器的一些bug,不必花太多时间在这方便。

CGFloat转化为CGFloat

优先级:低

下面的代码,将一个值为CGFloat的转化为CGFloat时反而会导致编译时长的大大增加。这是一个很奇怪的现象,估计是编译器的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()

优先级:低

下面的代码只是调用了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

Speed up Swift compile time

Regarding Swift build time optimizations

Swift build time optimizations — Part 2

Improving Swift Compilation Times from 12 to 2 Minutes

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容