Xcode构建过程的后台工作(WWDC2018字幕搬运)
原文传送门
Xcode构建过程的后台工作(二)clang构建
Xcode构建过程的后台工作(三)swift构建
Xcode构建过程的后台工作(四)链接
什么是构建过程
在构建app的时候,从源代码和项目资源开始,到提供给客户的打包文件或者上传到AppStore,要经过很多步骤。你要编译和链接源码,复制和处理资源,比如头文件、资源目录和storyboard,最后是代码签名及自定义脚本或者make文件,比如给框架构建API文件、运行代码检查和验证工具等等。大多数任务在构建过程中由命令行工具运行,比如Clang,LD,AC工具,IB工具,代码符号等。这些工具等执行,需要一组特定的实参,以特定的顺序,基于Xcode项目配置。
$ swiftc -module-name PetWall -target arm64-apple-ios12.0 -swift-version 4.2 ...
$ clang -x objective-c -arch arm64 ... PetViewController.m -o PetViewController.o
$ ld -o PetWall -framework PetKit PetViewController.o ...
$ actool --app-icon AppIcon ... Assets.xcassets
$ ...
$ [thousands more]
构建系统的作用,就是将每次构建的任务的执行部署自动化,由于任务数量成千上万,构建过程更是数不胜数,依赖关系十分复杂,你肯定不想手动输入,1天敲100遍命令,那就让构建系统帮你做。
构建任务执行顺序
构建任务的执行顺序取决于信息的依赖关系,就是任务,任务需要的输入,和任务生产的输出。以编译任务为例,它需要输入源代码文件,如PetViewController.m,然后输出目标文件,比如PetViewController.o。
同样,链接服务需要几个目标文件,这些文件由编译器在上个任务中生成,再生成可执行或lib文件,比如PetWall运行文件,会存到.app资源文件包。
你能看到信息的依赖关系,是顺着下图的走向,最终形成执行顺序。
现在大家关注下图中的编译任务,很像川流不息的马路,你看得到编译任务在各自的路上并行运行互不干涉。因为链接器任务需要所有的其他输入,所以它要在最后一位。
通过依赖关系构建系统
构建系统的第一步是获取构建描述,Xcode项目文件,解析项目中的所有文件,目标app和依赖关系,构建设置。转换成一个树形结构叫做定向图,它显示了所有的依赖关系,项目中的输入和输出文件,以及处理他们的执行任务。
然后低级执行引擎会处理这张图,研究依赖关系,决定执行哪个任务,执行顺序是什么,以及哪些可以平行运行,然后继续执行任务。这里的低级执行引擎是新构建系统的叫做llbuild,他是开源的,用GitHub开发。如果对构建系统有兴趣,请随意研究,看看它如何工作。它的链接和另一个关于构建系统的开源模块会在最后提到。
现在讲讲已知的依赖关系。由于你无法获取太多的依赖关系信息,构建系统在任务的执行过程中可能会找到更多信息。比如clang编译oc文件时会生成目标文件,但是它也会生成另一个文件,其中包含一个列出源文件中头文件的列表,那么下次构建时,构建系统会使用这个文件中的信息,以保证当你你改了其中任何头文件时会再次编译源文件。
这里的关系路径是PetController.h,PetController.d,.m直到.o文件。
构建系统的主要工作就是执行任务。当然项目越大,构建时间越长。你肯定不想在每次运行的时候把所有任务都运行一遍。构建系统实际上可以只执行定向图上的任务子集,基于你对于项目的更改,对比你上次的构建,我们称之为累加构建。
准确的依赖关系十分重要,这样累加构建才能正确高效的工作。下面看看哪些更改会影响构建系统,以及与累加构建的关系。
更改检测和任务签名
构建过程中的每个任务都有相应的签名,类似于Hash,通过计算多个任务相关信息而得出。这些信息包括任务输入的统计信息,比如文件路径和更改时间标签,运行命令的命令行指示,以及其他有关任务的元数据,比如编译器版本。构建系统会追踪当前和之前的任务签名,所以它知道每次构建时是否需要重新运行任务。如果某个任务的签名与上次构建时不同,它就会重新运行这个任务,如果相同就会跳过,这就是累加构建的概念。
如何利用构建系统
我们大概了解了构建过程的定义和流程,那么如何利用构建系统呢?先回顾下基本知识。构建系统按照一定顺序执行一系列任务,但要记得构建过程以定向图表示。我们不用担心任务执行的顺序,这是构建系统的工作。作为开发者,我们需要考虑的是任务之间的依赖关系,让构建系统根据定向图结构决定最佳的执行方法。这样构建系统可以正确地给任务排序,可能的时候并行运行,以完全利用多核硬件
依赖关系的来源
对于某些任务,依赖关系来自构建系统自带的数据。构建系统自带一些规则,比如编译器,链接器,资源目录,storyboard处理器等等。这些规则定义了哪些是输入文件,和哪些是输出文件。
还有目标依赖关系(target dependencies),大致决定了目标构建顺序。有些时候,构建系统可以编译不同目标和平行文件,之前的Xcode要构建一个app就要完成整个app的构建,然后才能使用。Xcode 10的新构建系统就要快得多,编译源阶段会提前开始,免费提供并行。但是,如果含有任何运行脚本阶段,这些阶段完成后,并行才能开始。有关依赖的还有隐形依赖关系(Implicit dependencies),例如,如果您在链接库中列出目标时,在方案编辑器中启用了二进制构建阶段和隐式依赖项(这是默认开启的),那么即使它未在目标中列出,构建系统也将建立对该目标的隐式依赖关系。
接下来是构建阶段依赖(Build phase dependencies)。在目标编辑器里,你会看到几个构建阶段,复制头文件、编译源、复制资源包等等。这些任务与每个阶段相关,通常根据阶段的排列顺序按组运行。如果有更好的方案,构建系统也许会忽略它,例如第三方静态库阶段,在编译源之前。注意有的时候构建阶段顺序不对会导致问题或者构建失败,因此请确保了解您的依赖关系并验证构建阶段是否正确。
还有scheme顺序依赖。如果在scheme设置里开启了并行构建检查,构建性能会更好,不用担心目标顺序。但是如果关闭并行构建,Xcode构建目标时会按照你排列的构建行动顺序逐个构建,目标依赖关系优先级较高,优先决定第一个构建目标,但Xcode会遵从这个排列。这让人跃跃欲试,因为它给出了可预期的构建顺序,即使依赖关系有误。但这样会牺牲大量并行空间,延缓构建速度。所以我们推荐开启并行构建,正确设置依赖关系,不要依赖排序。
最后,依赖关系取决于开发者。你可以自定义shell脚本构建阶段或规则,明确告诉系统输入和输出是什么,以避免重复运行不必要的脚本任务,保证正确执行顺序。你可以用运行脚本阶段编辑器定义输入和输出,这些文件路径将作为环境变量在脚本中激活。不要依赖项目里目标依赖关系的自动连接。Clang编译器里有自动关联功能,在构建设置中自动使用关联框架。让编译器自动链接框架对应导入的模块,不用在链接库的构建阶段再明确表示。但是要注意自动关联不会在构建系统层级建立依赖关系,所以它不能保证依赖的目标在关联之前已经建好。所以它只适用于STK平台的框架,例如Foundation或者UIKit,因为我们知道它们在构建前就已经存在。你自己项目里的目标,要保证明确的库依赖关系。你也许需要创建项目引用,将另一个Xcode项目拖放到项目文件导航,说明与其他项目的目标文件的依赖关系。
总结来说,有了准确的依赖关系,构建系统就能更好的并行构建任务,保证每次的构建结果一致。这样就能减少构建用时,给开发多点时间。想知道更多快速构建的内容,如何最大化利用新的iMac pro内核,推荐观看演讲《用Xcode加速构建过程》。