Xcode构建过程的后台工作(一)构建过程
Xcode构建过程的后台工作(二)clang构建
Xcode构建过程的后台工作(四)链接
构建过程:Swift构建
我们现在将深入研究Swift和构建系统如何协同工作以在项目中查找声明。Clang单独编译了每个Objective-C文件。这意味着如果要在另一个文件夹中 查找一个类,则必须导入声明该类的头文件。但Swift的设计不需要写入头文件。这使初学者更容易上手该语言。 并避免了在不同文件中重复一个声明。但这意味着编译器必须执行一些额外的记录工作。下面讲讲记录工作如何运作。让我们回到示例PetWall应用程序。
该应用程序在ViewCcontroller中 有一个Swift界面。Objective-C app代理,和Swift单元测试。 为了编译哪怕只是上面这个PetViewController部分,编译器也要执行四种不同的运算。首先,它必须找到声明,既可以在Swift目标内,也可以来自Objective-C。此外,它还要生成描述文件内容的 接口。这样它的声明就可以被Objective-C和其他Swift目标中找到并使用。 在接下来的部分中,我将通过这个例子来分别阐述这四个任务。首先是在Swift目标中查找声明。编译PetViewController.swift时,编译器会查找 PetView初始程序类型,以便检查调用。但在此之前,它要解析PetView.swift并验证它,以确保初始程序的声明是正常的。
编译器很聪明 ,它知道它不需要检查初始程序的主体。但它仍然需要做一些工作去处理文件的接口部分。这与Clang不同,编译一个Swift文件时,编译器将解析目标中的所有其他Swift文件,以检查与接口相关的部分。
在Xcode 9中,这导致了在增量调试构建中的一些重复工作。因为编译器单独编译每个文件。文件能够并行编译,但它强制编译器重复解析每个文件。解析一次作为实现文件产生一个.o,解析多次是作为接口来查找声明。
Xcode 10减少了这种开销。它通过将文件合并成组,在依旧最大化并行的同时,尽可能多地共享工作来实现。
在组中重复利用解析,只在跨组处理时重复。由于组的数量通常相对较低,因此 可以显著加快增量调试构建。
从Objective-C中查找声明
Swift代码不仅调用其他Swift代码。 它也可以调用Objective-C。回到我们的PetWall示例应用程序,我们可以看到,这是至关重要的,因为UIKit等系统框架是用Objective-C编写的。Swift与其他语言不同,它不需要外部功能 接口。例如,您必须编写Swift声明给每个Objective-C API。但是,编译器内置了Clang的一大部分,并将其用作库。这样就可以直接导入Objective-C框架。
那么Objective-C声明来自哪里?导入器会根据目标的类型查找头文件。任何目标在导入Objective-C框架时,导入器在头文件中查找声明,以显示该框架的 Clang模块映射。在Swift和Objective-C代码混编的框架内,导入器在umbrella头文件中查找声明。这个头文件定义了公共接口。这样,框架内的Swift代码可以调用同一框架中公共Objective-C代码。最后,在您的应用和 单元测试中,可以导入目标的桥接头文件,允许其中的声明被Swift调用。
当导入器找到声明时,通常会修改它们,让它们更口语化。比如它会导入Objective-C方法,用NSError惯用语作为使用Swift的内置错误处理语言功能的throwing方法。特别是它会删除动词和介词后面的参数类型名称。 例如,drawPet:atPoint方法有单词pet,和跟在一个动词draw后的pet类型的参数。
同样的单词point,代表参数类型CGPoint,后跟介词at。
Swift中省略了这些单词,只导入函数draw at。怎么做到的?编译器包含常用英语动词和介词列表。 因为它只是一个硬编码列表而且人类语言很复杂,有时候它会缺词。
此外,为了匹配Swift的命名约定,导入器会重命名方法,根据词性删除单词。 例如,动词Feed不在列表中,因此feedPet不会像我们预期的那样作为Feed导入。发生这种情况时,您可以使用 NS_Swift_Name注解让编译器导入你要的方法形式。
如果您想查看如何将Objective-C标头 导入Swift,您可以到Xcode的相关 项目弹出窗口。
它在源编辑器的左上角。选择生成的接口,就能看到接口在不同版本的Swift中的样子。这就是Swift如何导入Objective-C。
生成在Objective-C中使用的接口
Objective-C中如何导入Swift?答案是Swift会生成一个头文件,可以进行导入。这允许您在Swift中编写类并从Objective-C中调用它们。让我们看看它是如何工作的。编译器将为扩展NSObject的Swift类和@obc标记的方法生成 Objective-C声明。
对于单元测试的app,头文件会包括公共声明和内部声明。这样就能在app的Objective-C部分使用内部Swift。但是,对于框架,生成的头文件只包含公共声明,因为它包含在构建产品中,是框架的公共接口的一部分。
在上图右侧,您看到编译器将Objective-C类链接到名字变形的Swift类,该名称包含模块名称PetWall。当两个模块定义了有相同名称的类时,这可以防止运行时发生冲突。 您可以通过将标识符传递给obc属性让Swift重命名Objective-C类。但是如果你这样做,你要确保名称不冲突。在这里,我使用了PWL前缀,以减少冲突的可能性。这样就可以在Objective-C中引用这个类PWLPetCollar 。
编译器采用类似的方法生成其他Swift目标的接口。为此,Swift基于Clang的模块概念进行构建,并且更深层的融入语言。在Swift中,模块是可分布的声明单元。为了能够使用这些声明,您必须导入模块。您可以导入Objective-C模块例如XEtest。在Xcode中,每个Swift目标都会生成一个单独的模块,包括您的app target。所以要导入app的主模块,以便进行单元测试。
导入模块时, 编译器会反序列化一个特殊的Swift模块文件,在使用它时检查类型。 例如在此单元测试中,编译器将从PetWall Swift模块加载PetViewController,以确保正确创建控制器。这类似于编译器在目标中查找声明的方式。
除此之外编译器加载一个汇总模块的文件,而不是直接解析Swift 文件。
编译器生成此Swift模块文件很多就像Objective-C头文件。但它不是文本,而是二进制表示。 它包括内联函数的主体,就像Objective-C中的静态内联函数或C++中的头文件实现一样。但是,有一点需要注意的就是它包含了私有声明 的名称和类型。这让您能在调试器中引用它们,很方便,但也意味着,您不能用私人秘密来命名私有变量。
对于累加构建, 编译器会生成部分Swift模块文件,然后将它们合并到一个表示整个模块内容的文件中。这种合并过程还可以生成单个Objective-C头文件。
在许多方面,这类似于链接器将目标文件整合成单个可执行文件时所执行的操作。 下文将主要讲述连接器。