如何提升 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 来隐藏没必要要暴露的头文件。

优化后的效果以下:

参考

相关文章
相关标签/搜索