最近在使用 Swift 开发项目时,发现编译时间实在是慢的出奇。每次 git 切换分支以后,都得编译很久,并且动辄卡死。有时候改了一点小地方想 debug 看下效果,也得编译那么好一下子,实在是苦不堪言。因此下决心要好好研究一下,看看有没有什么优化 Xcode 编译时间的好办法。git
本文中有很多实验数据,都是对基于现有项目进行的简单测试,优化效果仅供参考😅。github
第一步就是搞定编译时间的测算,方法以下。完成了以后就可进入正题了。swift
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
复制代码
module 是 Swift 文件的集合,每一个 module 编译成一个 framework 或可执行程序。在编译时,Swift 编译器分别编译 module 中的每个文件,编译完成后再连接到一块儿,最终再输出 framework 或可执行程序。后端
因为这种编译方式局限于单个文件,因此像有须要跨函数的优化等就可能会受到影响,好比函数內联、基本块合并等。所以,编译时间会变长。xcode
而若是使用全模块优化,编译器会先将全部文件合称为同一个文件,而后再进行编译,这样可以极大的加快编译速度。好比编译器了解模块中全部函数的实现,因此它可以确保执行跨函数的优化(包括函数内联和函数特殊化等)。缓存
另外,全模块优化时编译器可以推出全部非公有(non-public)函数的使用。非公有函数仅能在模块内部调用,因此编译器可以肯定这些函数的全部引用。因而编译器可以知道一个非公有函数或方法是否根本没有被使用,从而直接删除冗余函数。bash
####函数特殊化举例多线程
函数特殊化是指编译器建立一个新版本的函数,这个函数经过一个特定的调用上下文来优化性能。在 Swift 中常见的是够针对各类具体类型对泛型函数进行特殊化处理。架构
main.swiftapp
func add (c1: Container<Int>, c2: Container<Int>) -> Int {
return c1.getElement() + c2.getElement()
}
复制代码
utils.swift
struct Container<T> {
var element: T
func getElement() -> T {
return element
}
}
复制代码
单文件编译时,当编译器优化 main.swift 时,它并不知道 getElement
如何被实现。因此编译器生成了一个 getElement
的调用。另外一个方面,当编译器优化 utils.swift 时,它并不知道函数被调用了哪一个具体的类型。因此它只能生成一个通用版本的函数,这比具体类型特殊化过的代码慢不少。
即便简单的在 getElement
中声明返回值,编译器也须要在类型的元数据中查找来解决如何拷贝元素。它有多是简单的 Int
类型,但它也能够是一个复杂的类型,甚至涉及一些引用计数操做。而在单文件编译的状况下,编译器都无从得知,更没法优化。
而在全模块编译时,编译器可以对范型函数进行函数特殊化:
utils.swift
struct Container {
var element: Int
func getElement() -> Int {
return element
}
}
复制代码
将全部 getElement
函数被调用的地方都进行特殊化以后,函数的范型版本就能够被删除。这样,使用特殊化以后的 getElement
函数,编译器就能够进行进一步的优化。
状态栏 -> Editor -> Build Setting -> Add User-Defined Settings,而后增长 key 为 SWIFT_WHOLE_MODULE_OPTIMIZATION
,value 为 YES
就能够了。
Swift 默认设置是 Debug 时只编译 active 架构,Build active architecture only,Xcode 默认就是这个设置。能够在 Build Settings --> Build active architecture only 中检查到这一设置。
也就是说,在对每个文件单独进行编译时,编译器会缓存每一个文件编译后的产物。这样的好处在于,若是以前编译过了一次,以后只改动了少部分文件的内容,影响范围不大,那么其余文件就不用从新编译,速度就会很快。
而咱们来看一看全模块优化的总体过程,包括:分析程序,类型检查,SIL 优化,LLVM 后端。而大多数状况下,前两项都是很是快速的。SIL 优化主要进行的是上文所说的函数內联、函数特殊化等优化,LLVM 后端采用多线程的方式对 SIL 优化的结果进行编译,生成底层代码。
而设置 SWIFT_WHOLE_MODULE_OPTIMIZATION = YES
,全模块优化会让增量编译的颗粒度从 File 级别增大到 Module 级别。一个只要修改咱们项目里的一个文件,想要编译 debug 一下,就又得从新合并文件从头开始编译一次。理论上讲,若是单个 LLVM 线程没有被修改,那么也能利用以前的缓存进行加速。但现实状况是,分析程序、类型检查、SIL 优化确定会被从新执行一次,而绝大部分状况下 LLVM 也基本得从新执行一次,和第一次编译时间差很少。
不过注意,pod 里的库,storyboard 和 xib 文件是不会受影响的。
dSYM 文件存储了 debug 的一些信息,里面包含着 crash 的信息,像 Fabric 能够自动的将 project 中的 dSYM 文件进行解析。
新项目的默认设置是 Debug 配置编译时不生成 dSYM 文件。有时候为了在开发时进行 Crash 日志解析,会去修改这个参数。生成 dSYM 会消耗大量时间,若是不须要的话,能够去 Debug Information Format 修改一下。DWARF 是默认的不生成 dSYM 文件,DWARF with dSYM file 是会生成 dSYM 文件。
在 Xcode 9 中,苹果官方悄悄引入了一个新的编译系统,你能够在 Github 中找到这一个项目。这还只是一个预览版,因此并无在 Xcode 中默认开启。官方新系统会改变 Swift 中处理对象间依赖的方式,旨在提升编译速度。不过如今还不完善,有可能致使写代码时的诡异行为以及较长的编译时间。果真,我试了一下确实比原来还要慢。
若是想要开启试试的话,能够在 **File菜单 -> Working space ** Building System -> New Building System(Preview)
Generate dSYM | Who Module Optimization | 增长空行后第二次编译 | 首次编译 | 使用 New Build System | 编译总时间 |
---|---|---|---|---|---|
✔ | ✔ | 8m 42s | |||
✔ | 8m 18s | ||||
✔ | ✔ | ✔ | 2m 2s | ||
✔ | ✔ | 1m 36s | |||
✔ | ✔ | 0m 38s | |||
✔ | 0m 16s | ||||
✔ | ✔ | ✔ | 1m 26s | ||
✔ | ✔ | 0m 55s | |||
✔ | ✔ | 9m 24s | |||
✔ | ✔ | ✔ | 1m 46s |
let array = ["a", "b", "c", "d", "e", "f", "g"]
复制代码
这种写法会更简洁,可是编译器须要进行类型推断才能知道 array
的准确类型,因此最好的方法是直接写出类型,避免推断。
let array: [String] = ["a", "b", "c", "d", "e", "f", "g"]
复制代码
let letter = someBoolean ? "a" : "b"
复制代码
三目运算符写法更加简洁,但会增长编译时间,若是想要减小编译时间,能够改写为下面的写法。
var letter = ""
if someBoolean {
letter = "a"
} else {
letter = "b"
}
复制代码
let string = optionalString ?? ""
复制代码
这是 Swift 中的特殊语法,在使用 optional 类型时能够经过这样的方式设置 default value。可是这种写法本质上也是三目运算符。
let string = optionalString != nil ? optionalString! : nil
复制代码
因此,若是以节约编译时间为目的,也能够改写为
if let string = optionalString{
print("\(string)")
} else {
print("")
}
复制代码
let totalString = "A" + stringB + "C"
复制代码
这样拼接字符串可行,可是 Swift 编译器并不青睐这样的写法,尽可能改写成下面的方式。
let totalString = "A\(stringB)C"
复制代码
let StringA = String(IntA)
复制代码
这样拼接字符串可行,可是 Swift 编译器并不青睐这样的写法,尽可能改写成下面的方式。
let StringA = "\(IntA)"
复制代码
if time > 14 * 24 * 60 * 60 {}
复制代码
这样写可读性会更好,可是会对编译器形成极大的负担。能够将具体内容写在注释中,这样改写:
if time > 1209600 {} // 14 * 24 * 60 * 60
复制代码
在一个文件中,共减小了 2 处类型推断,一共优化 0.3ms,改进效果以下:
-- | 总时间 |
---|---|
更改前 | 135.3 ms |
更改后 | 135.0 ms |
所见 Xcode 对类型推断的处理优化仍是效果很不错的,并且在声明阶段的类型推断实际上并非很困难,所以提早声明类型其实对编译时间的优化效果影响不大。
在一个文件中,共减小了 2 处使用三目运算符的地方,一共优化 51.2ms,改进效果以下:
-- | 总时间 |
---|---|
更改前 | 229.2 ms |
更改后 | 178.0 ms |
可见使用三目运算符的地方会对编译速度产生必定的影响,所以在不是特别须要的时候,出于编译时间的考虑能够改写为 if-else 语句。
在一个文件中,共减小了 5 处使用 nil coalescing operator 的地方,一共优化 2.8ms,具体改进效果以下:
-- | 总时间 |
---|---|
更改前 | 386.4 ms |
更改后 | 178.0 ms |
根据结果而言,优化效果并不显著。但是根据前文所述,nil coalescing operator 其实是基于三目运算符的,那么为什么优化效果反而不如三目运算符?据我推测,缘由可能在于三目运算符只须要改写为 if-else 语句便可,而 nil coalescing operator 大部分时候须要先用 var 实现赋值语句,在使用 if-else 对赋值进行更改,因此总的来讲优化效果不大。
在一个文件中,共改进了 7 处字符串的拼接方式,一共优化 73ms,具体改进效果以下:
-- | 总时间 |
---|---|
更改前 | 696.1 ms |
更改后 | 623.1 ms |
可见改进字符串的拼接方式效果仍是十分明显的,并且也更符合 Swift 的语法规范,因此何乐而不为呢?
在一个文件中,进行了 5 处修改,一共优化 4952.5ms,效果十分显著。具体改进效果以下:
-- | 总时间 |
---|---|
更改前 | 5106.2 ms |
更改后 | 153.7 ms |
在一个文件中,进行了以前例子中的修改,一共优化 843.2ms,效果十分显著。具体改进效果以下:
-- | 总时间 |
---|---|
更改前 | 1034.7 ms |
更改后 | 191.5 ms |