WWDC 2018 Building Faster in Xcode

本文总结自 WWDC 2018 building faster in xcode

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

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

编译并行化

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

image

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

image

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

image

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

image

上面的串行 build 是如何变成并行 build 的效果的呢?以 Test Target 为例,它需要同时测试 Game、Shaders、Utilities 这三个组件,如果串行所需要花费的时间如下图所示

image

如果把三个组件分开来测试,效果就大不相同了。可以看到紫色的 Test Target的 build 时间提前了很多,这样 Test Target build 就可以和后续的其他任务并行,节省时间。

image

再者一点就是减少依赖暴露。Shaders target 依赖 Utilities,但是它可能只需要 Utilities 中一小部分代码和功能,那么我们可以进行剥离,这样一个小的改进将带来 build 速度大幅提升。可以看到下图,Code Gen 可以和 Physics 一起进行 build,提高了并发性。

image

测试未使用到的依赖。比如 Utilities 可能完全没必要依赖 Physics,如果解除他们之间的依赖关系,build 并行图会有新的变化,Utilities 的时机又可以提前,当 Code Gen build 完成,它就可以开始 build,和 Shaders 几乎在同一时刻并行 build

image

同时,Xcode 10 优化了 Target 之间的 build 过程,如果 TargetB 依赖 Target A,那么 TargetB 不需要 TargetA 完全 build 完成,就可以开始 build了,只要保证 TargetB 所需要 Code build 完成即可,这样 TargetB 就可以更早的开始 build。但是如果 Target 在 build phases 有配置执行脚本 ,那么必须要等待脚本执行完成才可以。

image

Run Script Phases

在 build phases 中配置执行脚本可以让我 Xcode 按照我们的需要定制 build 过程,如下所示,添加的脚本的时候,可以指定脚本或者脚本路径、输入文件、输出文件。

image

这个脚本有几个固定的执行时机,分别是

  • No input files declared (没有声明输入文件)
  • Input files changed(输入文件发生改变)
  • Output files missing(输出文件缺失)

我们应该指定 input files 和 output files,因为如果不指定,Xcode 就会每次增量编译的时候执行一次这个 build 脚本,增加了 build 的时间。

image

依赖循环是很常见的,Xcode 10 提供了很好用的诊断机制和详细的文档

image
image

测量编译的时间

我们可以通过 Xcode 的 log 显示每个 Target 的编译时间和链接是多少

image

同时 Xcode 10 提供了一个新的 feature,就是 Timing Summary,可以通过点击 Product -> Perform Action -> Build With Timing Summary 进行编译,这样在 Build Log 的末尾就会添加 Timing Summary Log。可以看到第一条就是 Phase 脚本的执行时间 5.036 秒,我们可以通过这个 log 看到哪个阶段是耗时的,便于我们进行优化。

image

Timing Summary 在终端也是可以使用的,只要加上 -showBuildTimingSummary 标记即可

image

源码级别的优化

首先讲了一个 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)) }.
}.

编译器会报错:

image

受到上个示例的启示,我脑海中首先想到的就是为 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 文件和工程依赖

增量编译是基于文件的,假如原始依赖如下图所示

image

此时在左面的文件的 Struct 内部做修改,Swift 编译器只会重新编译器左面的文件,而不会重新编译右侧的文件

image

但是如果在左侧文件增加了新的 API,虽然并不会影响右侧的文件正常编译,但是编译器是保守的,也会重新进行编译。

image

在一个 target 内部文件的更改不会影响其他 target,只需要重新编译该 target 内部和该文件有依赖关系的文件即可。

如果一个文件在 target 内部有依赖,在其他 target 也需要依赖这个文件,对于这种跨 target 的情况,该文件修改,会影响所有的 target ,所有 target 都要进行重新编译。

image

减少 Objective-C/Swift 暴露的接口

对于一个混编项目来说,Objective-C Bridging Header 是 Objective-C 向 Swift 暴露的接口,Swift 生成的 *-Swift.h 代表的是Swift 向 Objective-C 暴露的接口。

image

对于下面的两个例子,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

image

对于混编项目,减少 Objective-C 的接口暴露也是必要的,比如对于下面这种情况,Bridging-Header 暴露了 myViewController,但是 myViewController内部又引用了其他头文件,可能 networkManager 在 Swift 中并没有使用到,那么这样暴露 myNetworkManager 就完全没有必要了,可以使用 Category 来隐藏不必要暴露的头文件。

image

优化后的效果如下:

image

参考

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

推荐阅读更多精彩内容

  • 嗯哼嗯哼蹦擦擦~~~ 转载自:https://github.com/Tim9Liu9/TimLiu-iOS 目录 ...
    philiha阅读 4,658评论 0 6
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,612评论 4 59
  • 若无其事地缓缓走入教室,几乎是下意识地去了靠窗的一个位子坐下,然后习惯性地朝窗外张望片刻。这一系列动作就如事先预演...
    Yuki佳木阅读 178评论 0 0
  • 蒸腾的夏夜热浪在灯箱上 催促一种声音走出记忆 在独守寂静中的面包房 永无仿佛虚构的你 / 有些人只会消失在记忆里 ...
    唐啵儿阅读 204评论 0 1
  • (连载)《美梦如璇·第八章·第七十九节》 有一段日子,我披上盔甲,以为战无不胜。 有一段日子,我褪下战衣,以为岁月...
    叶子程阅读 329评论 1 0