阅读 2341

如何提高 Xcode 的编译速度

本文总结自 WWDC 2018 building faster in xcode

该 Session 通过一系列的实践来实现 Xcode 的快速编译,共阐述了六个大方面,分别是:

  • 将编译过程并行化
  • 通过指定输入、输出文件减少脚本的重复编译
  • 测量你的编译时间,找到优化的突破点
  • 理解 Swift 文件和工程之间的依赖
  • 处理复杂的表达式
  • 减少 Objective-C 和 Swift 暴露的接口

编译并行化

通常,我们的 Target 都会显式依赖其他 Target,在链接的时候会隐式链接其他很多库(Library)。以一个游戏的依赖为例,Tests Target 会依赖 Game、Shaders、Utilities,同时 Game 也需要依赖 Shaders、Utilities、Physics。

如果他们的 build 顺序是按照串行顺序,那么他们的构建顺序和时间如下,他们之间需要等待前一个 build 完成后才可以继续,是非常耗时的,浪费了工程师们太多的时间。

如果采用并行 build,则会节省大量的时间,效果如下图所示。此次 build 过程并没有减少工作量,但是时间却减少了很多。

那么如何实现并行 build 呢?可以在 Xcode 中进行配置完成。点击 Target,然后点击 Edit Scheme,点击 build 配置,勾选 paralielize Build 和 Find implicit Dependencies 选项。

上面的串行 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 几乎在同一时刻并行 build

同时,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 来隐藏不必要暴露的头文件。

优化后的效果如下:

参考

关注下面的标签,发现更多相似文章
评论

查看更多 >