
Xcode 10 中全新的构建系统完全采用 Swift 编写而成, 提供了更好的性能和稳定性。
对于 Xcode 中的构建过程,你是否有过这些疑问?
当你在 Xcode 中按下快捷键 Command + B 时,构建过程如何进行组织?
Xcode 如何根据项目中的文件信息来决定构建过程?
接下来,让我们一起进入编译器的国度并深入探索以下内容:
- Clang 和 Swift 如何将源代码构建成 object 文件
- headers 和 modules 如何工作
- 编译器如何在代码中寻找声明
- Swift 的编译模型和 C, C++, Objective-C 在本质上的区别
- symbols 是什么,及其如何与我们的源代码进行关联
- 为了构建你的应用或者框架,linker 如何将编译器产生的 object 文件黏结在一起并生成可执行文件
内容概览
- 构建系统
- Clang 编译器
- Swift
- Linker
我们将使用构建 PetWall App 的过程来作为示例。这个示例将贯穿整个讲解过程。
PetWall 是一个用来展示宠物照片的小应用。

构建系统

让我们先来理解什么是构建过程以及构建过程在 Xcode 中构建像 PetWall 这种典型应用时是如何运作的。
你可以看到项目中有 app target、 一个 framework 、以及用 Swift 和 Objective-C 编写的源代码。或许这看起来就像你自己的项目。

你需要编译、链接源代码,还需要复制和处理资源文件,比如:头文件,assets 还有 storyboards。最后还需要对代码进行签名,甚至还要用脚本来做一些自定义的工作,或者为项目构建文档,执行代码检查工具等。

现在,很多构建过程中的工作都需要通过命令行工具来完成。
比如:Clang, LD, AC 工具, IB 工具, 代码签名 等等。

这些工具的执行都需要有具体的参数,而且参数的顺序基于 Xcode 项目的配置。
所以,构建系统所做的工作就是在构建项目时自动组合以及执行这些工具。
由于构建过程可能涉及到超过数以万计的任务,而且任务之间可能有复杂的依赖关系,你绝对不希望自己手动地去执行这些工具。

让构建系统来为你做这些琐事吧!

现在,让我们来探讨一下构建过程的顺序是如何决定的,以及为什么它很重要。
构建任务的顺序取决于任务的依赖信息,任务需要消耗的输入,以及产生的输出。

比如一个编译任务需要消耗 PetController。
所以需要把 PetController.m 作为输入, 然后输出 object 文件 PetController.o。

类似地,一个链接任务消耗一些在之前的任务中由编译器生成的 object 文件,产出一个可执行文件或者库文件。
比如这里的 PetWall 最终会被添加到 app bundle 中。

希望你已经看出来一些规律。

你可以发现依赖信息如何贯穿了整个结构图,并决定了执行顺序。
如果你仔细观察编译任务,它们就像公路上的多条车道一样。
每个编译任务在自己的车道里是完全独立的,所以就可以被并行地执行。
因为链接任务将其他所有产出物作为输入,所以它被放到最后来执行。
所以,构建系统使用依赖信息来决定任务的执行顺序,以及哪些任务可以并行执行。我们把这叫作 依赖顺序。
现在,我们已经知道什么是构建过程,让我们来探究一下它是如何工作的。
当你选择构建项目时,构建系统会从项目文件中获取构建描述信息。
然后,解析项目文件,获取你项目中的所有文件、target 及其依赖关系、build settings,最后把它变成一个树形结构(有向图)。

构建系统通过使用有向图来表示项目中的输入文件和输出文件之间的依赖关系以及用来处理这些文件的任务。

接下来,底层执行引擎(已在 GitHub 开源的 llbuild) 开始处理这个有向图,查看依赖说明并算出任务执行的顺序以及哪些任务可以并行执行,然后开始执行这些任务。
通常你的项目不会有太多依赖信息,所以构建系统会在任务的执行过程中去发现更多信息。
比如,当 Clang 在编译一个 Objective-C 文件时,它会生成一个你期望的 object 文件。
但是它也可以同时生成另一个文件,这个文件中有一个记录了被该原文件包含的所有头文件的列表。

当下一次你再构建时,如果你更新了任何一个被这个源文件包含的头文件,构建系统使用这个文件中的信息来确保这个源文件也被重新编译。

现在,我们已经清楚地知道构建系统的主要工作就是执行任务。
当然,项目越大,执行的时间就会越久。
所以你肯定不希望每次构建时都执行所有的任务。
构建系统只会执行有向图中的部分任务,这取决于项目的最新更改与上一次构建之间的差别。
我们把这叫作 增量构建。准确的依赖信息决定了增量构建是否正确并且高效地运行。
现在,我们已经知道改动会影响构建系统,以及改动与增量构建之间的关系。
那么,构建系统如何检测到改动呢?

- 构建过程中的每个任务都有一个关联的签名,这个签名是根据任务相关的多种信息计算得出的哈希值。
- 这些信息包括任务的输入,比如:文件路径、修改时间戳等。还有具体任务的元数据也会被使用,比如:编译器的版本等。生成这个签名的任务由命令行工具来完成。
- 构建系统会记录当前构建任务以及前一次构建任务的签名,所以它可以在构建时决定哪些任务需要重新运行。
- 如果当前构建任务的签名与前一次构建任务的签名不同,构建系统就会重新运行这个构建任务。如果相同,则跳过。这就是增量构建的基本原理。
现在我们已经对构建系统有所了解,那么我们如何帮助构建系统完成它的工作呢?
让我们来回顾一下基础部分。
一个构建过程就是一系列任务执行的特定顺序。
但是请注意,这个构建过程是使用有向图来记录的!

所以,我们不需要记住任务执行的顺序,那是构建系统而不是我们应该做的事情。

相反,作为一个开发者,我们需要捋清楚任务之间的依赖关系,然后让构建系统根据有向图的结构更好地去执行这些任务。
这可以使构建系统正确地执行任务,并且尽可能充分地利用多核硬件的优势。
那么,依赖从何而来?
- 构建系统内部的依赖:
对于某些任务,依赖来源于构建系统本身。构建系统内置了编译器、链接器、asset 目录、storyboard 处理器等相关的规则。这些规则定义了什么类型的文件可以作为输入或者输出。

- Target 依赖(Target Dependencies):
Target 依赖
大致决定了 target 构建的顺序。在某些情况下,构建系统可以并行地构建不同 target 的编译资源。在 Xcode 10 之前,构建一个 target 时首先需要彻底完成这个 target 所依赖的 target 的构建过程。在 Xcode 10 及以后,target 能更早开始构建。这意味着你的编译资源阶段可以更早开始。然而,如果你定义了自定义构建脚本,在这些脚本需要执行结束之后,并行化执行过程才会开始。

- 隐式的依赖:

- 构建阶段(Build Phases)的依赖:
构建阶段包括:复制头文件、编译资源、复制 bundle 资源等。
这些任务会根据自身所处的阶段的顺序被分组执行。如果构建系统对构建阶段全面知晓,它可以忽略这个顺序。需要注意的是,错误的构建阶段顺序会导致构建问题或失败。所以你要确保自己理解依赖,确定构建阶段的顺序是正确的。

- Scheme 顺序依赖:
如果你为 scheme 开启了并行构建(parallelize build),你将获得更好的构建性能,而且 scheme 中的 target 构建顺序也不再重要。这个选项是默认开启的。
然而,如果你关闭了这个选项,Xcode 会根据 target 的顺序依次进行构建。Target 依赖
可以优先决定哪些 target 先被构建。除此之外,Xcode 会按照顺序进行构建。
推荐开启并行构建,并且正确设置Target 依赖
。

- 自定义任务:
如果需要在构建阶段运行自定义的脚本或者规则,需要定义输入和输出。这样可以避免构建系统重复运行没必要的脚本任务。


- target 依赖自动链接
不要依赖自动链接,这个设置项允许编译器在没有显式链接你的库的构建阶段的情况下去链接所有你已经导入的库。值得注意的是,自动链接不会构建系统的层级中建立库的依赖。所以它不保证你依赖的 target 在你链接前就已经构建完成。所以你应该只对系统提供的库开启这个选项,比如: Foundation, UIKit 等。因为我们知道它们在我们构建项目之前就已经存在。对于你的项目中的 target,要确保添加显式的库依赖。甚至你还有可能需要通过在 Xcode 中将其他项目拖拽到当前项目中建立项目引用,然后使用其他项目中你所依赖的 target。

总之,凭借准确的依赖信息,构建系统可以更好地以并行的方式执行你的构建过程,并确保每次构建结果的一致性。从而,帮你节省更多构建时间,让你有更多时间去进行开发。
想加速你的构建过程以及充分利用多核硬件的优势?
通过 《Building Faster in Xcode Session》 了解更多内容吧!
参考内容:
Behind the Scenes of the Xcode Build Process
转载请注明出处,谢谢~