(WWDC) Xcode 构建过程的幕后 —— Swift

现在,让我们深入到 Swift 如何与构建系统协作查找声明代码的细节中。

首先,我们需要回顾一个要点:Clang 会分开编译每一个 Objective-C 文件。
如果你需要引用一个文件中的某个类,你必须导入声明了这个类的头文件。

然而,Swift 不需要你导入头文件。这明显减轻了初学者的负担,也避免让你重复导入声明。
不过,这就意味着编译器需要做更多的工作。

让我们回到之前的例子, PetWall App。

这个应用有一个用 Swift 编写的 ViewController,
还有一个用 Objective-C 编写的 AppDelegate,
还有用 Swift 编写的单元测试。

 

为了编译 PetViewController 这个文件,编译器需要做四种不同的操作。

  • 查找声明
    • 来自 Swift Target
    • 采用 Objective-C 编写
  • 生成接口
    • 用于 Objective-C
    • 用于其他 Swift Target

 
 

首先,在 Swift Target 中寻找声明:

在编译 PetViewController.swift 时,编译器需要找到 PetView 的初始化方法,然后才能检查这个调用。
但是在此之前,编译器需要先解析并且验证 PetView.swift 来确保 PetView 的初始化方法调用正确。

编译器不需要解析 PetView 初始化方法的函数体部分,不过也要处理 PetView.swift 的接口部分。

Clang 会解析 Target 中的所有源文件

这与 Clang 的解析过程不同,Clang 会解析这个 Target 中其他的 Swift 文件来验证这些部分和接口相关联。

在 Xcode 9 中,这会导致重复的工作,因为编译器单独编译每个文件。
这样可以使文件被并行编译,不过这也致使编译器重复解析文件。

Xcode 10 通过将文件进行分组解析来共享工作成果,这样就可以有效地减少重复解析的开销,同时尽可能最大化地允许并行化。

这些解析在分组内重用,在跨分组时才需要重复解析。
相对来说,分组的数量比较少。所以这可以显著提升增量构建(Incremental Build) 的速度。

  

然而,现在的 Swift 代码还会调用 Objective-C 代码。

让我们回到 PetWall App,我们知道 UIKit 系统库是采用 Objective-C 编写的。
Swift 采用了与其他语言不同的方式,它不需要你提供一个外部函数接口。

Swift 将大部分的 Clang 作为自己的内部库,这样就可以直接导入 Objective-C 框架。

  

那么,Objective-C 声明来自何处?

  

当你在任意的 Target 中导入一个 Objective-C 框架时,头文件中的声明会暴露 Clang 模块映射,导入器会查找这些声明。
在 Swift 和 Objective-C 混编的框架中,导入器会在伞头(umbrella header)中查找声明,伞头中定义了公开接口。
通过这种方式,同一个框架中的 Swift 代码可以调用同一个框架中的公开的 Objective-C 代码。
最后,在你的应用或者单元测试中,你可以在桥接头文件里添加导入指令(import)。

导入器会做适当的调整,比如:将使用 NSError 的 Objective-C 方法转换为抛出错误的 Swift 方法并去掉一些参数、参数名。

比如,drawPet: atPoint 方法 中的 Pet, Point 都会被移除。

你可能会猜想,编译器内部是不是有一个常用英语动词、介词表。

没错,的确有这样一个表!但是,也会有一些词没有被收录进来。

比如,feed 就没有被收录。

不过,你可以使用 NS_SWIFT_NAME 自定义转换后的方法名。

如何确认转换后的方法名符合预期呢?

 
 

现在来思考另一个问题,Objective-C 如何导入 Swift 代码呢?

 
 

实际上,编译器会为 Swift 生成一个头文件。这个头文件中会包含继承自 NSObject 的类和被 @objc 标记的方法的声明。

对于单元测试中的应用,头文件会包含公开(public)和内部(internal)的声明,这可以允许你在 Objective-C 中 使用 Swift 内部的方法和属性。
对于框架,生成的头文件中只会包含公开的声明。

你会发现,生成的 Objective-C 头文件中的 Swift 类名是乱码,其中包含了类名和模块名。
这样可以有效地避免由于不同模块定义了相同类名导致的运行时错误。

当然,你也可以显式地指定一个名称。但是,如果这样的话,你需要自己为命名冲突问题负责。

在 Swift 中,一个模块是一个可分配声明单元。

为了使用这些声明,你需要导入模块。
每个 Swift Target 产生单独的一个模块。当然,你的 App target 也是如此。
这就是为什么上面的示例代码需要在单元测试中导入 PetWall。

当导入一个模块时,编译器会反序列化一个特殊的 Swift 模块文件来检查你用到的类型。
比如在上面的单元测试代码中,编译器会加载 PetWall.swiftmodule 中 PetViewController 的部分来确保你创建 PetViewController 的代码是正确的。

这和上文中讲述的编译器如何在 Target 中查找声明的方式类似。除此之外,编译器会加载一个概述这个模块的文件,而不是单独解析每个文件。这类似于生成 Objective-C 头文件,不过这个文件的内容不是文本格式而是二进制格式。其中包含内联函数的函数体(类似于 Objective-C 中的静态内联函数或者 C++ 中的头文件实现)。

值得注意的是,这其中也包含了私有声明的名称和类型。这可以允许你在调试器中引用,确实相当方便!

对于增量构建,编译器会产生部分的 Swift 模块文件,然后合并到代表整个模块的内容的那个文件中。
这个过程使生成单个 Objective-C 头文件变成了可能。

这在很多方面类似于链接器如何将多个文件链接生成单个可执行文件的过程。

  
继续阅读 (WWDC) Xcode 构建过程的幕后 —— Linker





参考内容:
Behind the Scenes of the Xcode Build Process




转载请注明出处,谢谢~

推荐阅读更多精彩内容