![](https://img.haomeiwen.com/i1306450/7497c8ea9ca3a438.png)
我们将会了解到和 Clang 相关的两个特性:
- 如何使用头文件映射(header map)来将 Xcode 构建系统产生的信息传递到 Clang 编译器;
- 如何使用 Clang 模块(modules)来加速构建;
什么是 Clang ?
苹果公司的 C 语言家族的官方编译器:
- C
- C++
- Objective-C
- Objective-C++
其实,Swift 也需要使用 Clang。
编译器会为每一个输入文件生成一个之后用于链接的输出文件。
如果你需要访问一个 iOS API 或者调用你自己的实现,你通常需要在代码中包含一个头文件。
![](https://img.haomeiwen.com/i1306450/16e115d984bfff1f.png)
头文件就是一个承诺,承诺实现部分存在而且与声明内容相符。
![](https://img.haomeiwen.com/i1306450/6ee080355e963bb2.png)
如果你只更新了头文件但是忘了更新相应的实现,在编译阶段是不会发生错误,但是在链接阶段会出现问题。
现在,让我们结合实例程序 PetWall 来理解如何处理头文件。
![](https://img.haomeiwen.com/i1306450/d4324c20bed4d000.png)
实例程序 PetWall 内部混合使用了多种语言。
![](https://img.haomeiwen.com/i1306450/1cc9a6a5009f7fbb.png)
程序主体部分采用了 Swift 编写。
![](https://img.haomeiwen.com/i1306450/fbc7608c16365c8c.png)
使用了一个用 Objective-C 编写的库 PetKit。
![](https://img.haomeiwen.com/i1306450/ee6a4b019b2956a7.png)
还使用了一个用 C++ 编写的库 PetSupport。
现在,这个应用的体积随着时间开始膨胀。
我们决定把 Cat 相关的文件移到另一个文件夹中,但是不改动任何实现文件。
![](https://img.haomeiwen.com/i1306450/1201b4efb8fed099.png)
然后,项目还是能正常编译和运行。
你可能会好奇 Clang 如何找到头文件呢?让我们来看一个例子。
![](https://img.haomeiwen.com/i1306450/b8a19467e161223b.png)
这是其中一个包含了 Cat.h 头文件的实现文件。
但是,我们如何查明 Clang 做了什么?
![](https://img.haomeiwen.com/i1306450/236dad9ae960a7c2.png)
打开构建日志,查看 Xcode 构建系统如何编译这个文件。
复制这个指令,然后粘贴到命令行终端中,并在结尾添加 -v
参数。 -v (verbose) 的意思是冗长的。
然后 Clang 就会告诉你很多信息。
不过,我们只需要关注 搜索路径(search paths)
。
搜索路径指向了你的源代码,但这并不是你猜想的答案。
![](https://img.haomeiwen.com/i1306450/00da15f569cd5dea.png)
Xcode 构建系统使用 headermap
来记录头文件的位置。
![](https://img.haomeiwen.com/i1306450/9d802e7b5a375b91.png)
使用 headermap
是为了指向源代码,当 Clang 需要生成警告或者错误时,它可以给出具体的源代码位置。
因为很多人不知道 headermap
的用处,所以遭遇了很多问题。
一个常见的问题就是 忘记将头文件添加到项目中,头文件只在目录中而不在项目中。
所以,记得把头文件添加到项目中。
另一个是 头文件重名问题,尽可能地为头文件添加唯一的文件名。
如果你有一个头文件的名称和系统的头文件同名,就会屏蔽掉系统的头文件。
说到系统头文件,现在就以 PetWall 项目为实例讲解如何找到系统头文件 Fundation.h
。
![](https://img.haomeiwen.com/i1306450/fa89e74ed0bc4bb0.png)
忽略项目中的头文件,只看 $(SDKROOT)
相关的路径:
![](https://img.haomeiwen.com/i1306450/7ef923549b59728d.png)
默认情况下,我们会在 SDK 的这两个路径中查找头文件。
![](https://img.haomeiwen.com/i1306450/c7c7378212b46e5d.png)
对于第一个路径,我们无法找到头文件,因为头文件不在那里。
对于第二个路径,因为这是一个框架路径,所以 Clang 的行为略有不同。
首先,它需要知道这是什么框架以及框架是否存在。
![](https://img.haomeiwen.com/i1306450/2a03ec33c0fcb30e.png)
然后,它需要在头文件目录找到头文件。
对于这个例子,它确实可以找到头文件。
![](https://img.haomeiwen.com/i1306450/602b964375837dff.png)
如果找不到呢?Clang 会去查找私有头文件。
苹果发布的SDK中不会包含私有头文件,但是你的项目中包含公开和私有头文件,所以 Clang 会去查找。
![](https://img.haomeiwen.com/i1306450/c8eeba46df337295.png)
如果这个头文件确实不存在,那么 Clang 就会停止查找。
如果你好奇生成的实现文件(所有相关的头文件被导入而且被预处理之后)是什么样,你可以让 Xcode 为你预处理源代码文件。这个操作会生成一个庞大的输出文件。
![](https://img.haomeiwen.com/i1306450/bf816b7cb851cdf9.png)
这个文件会有多大?
![](https://img.haomeiwen.com/i1306450/93a99d055b4f94ea.png)
Foundation.h
是系统中最最基础的头文件,所以你常常会直接或者间接地导入它。
这也就意味着,每一次编译调用(compiler invocation)都会去查找这个头文件。
目前为止,每一次导入 Foundation.h
, Clang 都需要处理超过 800
个头文件。
在每一次编译调用中,差不多有 9 MB
的源代码文件需要被处理和验证。
这么大量的重复工作,应该想方设法避免。那么我们该怎么做?
其中一个你可能已经知道的解决方案是预处理头文件(precompiled header files)。
但是,我们还有更好的解决方案:Clang 模块 (Clang modules)。
Clang 模块允许我们为每个框架只进行一次查找和解析,然后缓存在磁盘上以便后续的重用。
这可以有效地优化你的构建时间。
为了实现这个目的,Clang 必须包含某些属性。其中一个最重要的属性就是上下文无关。
![](https://img.haomeiwen.com/i1306450/d5d7f3e8733cd556.png)
如果像上面这样使用传统的宏定义来导入头文件,我们将无法重用模块。
相反,模块会忽视这些上下文相关的信息,以此来实现模块的重用。
另一个要求是模块需要制定所有的依赖(自包含)。
这样做有一个优点,当你在导入一个模块时就可以开始使用这个模块,你不用担心是否需要添加其他的头文件才能使模块正常工作。
那么,Clang 怎么知道是否应该构建一个模块呢?
让我们来看一个与 NSString.h 相关的简单例子。
![](https://img.haomeiwen.com/i1306450/1a01f9cb2425a33c.png)
首先,Clang 不得不在框架中先找到相应的头文件,我们已经知道需要在 Foundation.framework 目录中查找。
接下来,Clang 编译器会查找模块目录和一个与头文件目录相关的模块映射
。
![](https://img.haomeiwen.com/i1306450/0773364133e009e1.png)
什么是模块映射?
一个模块映射用于描述一个特定集合的头文件如何转换到模块上。
![](https://img.haomeiwen.com/i1306450/9922b1c8c00dec1a.png)
你会发现,模块映射中只有一个头文件 Foundation.h
。
但是,这其实是一个特殊的头文件,它被 umbrella
关键字标记。(umbrella header 直译就是伞头,这样的表述简直生动形象)
umbrella
关键字说明,Clang 需要在这个特殊的头文件中去查明 NSString.h
是否是这个模块的一部分。
![](https://img.haomeiwen.com/i1306450/066fb85eed2ec763.png)
Clang 可以找到 NSString.h
,说明它是模块的一部分。
现在,Clang 可以将文本式的导入升级为模块导入,不过我们需要先构建 Foundation 模块。
那么,我们如何构建 Foundation 模块?
首先,我们为它创建一个单独的 Clang 位置。
这个 Clang 位置包含了来自 Foundation 模块的所有头文件。
我们不从原始编译器调用中传输任何现有的上下文。 因此,它是上下文无关的。
我们传输的只是命令行
传递给 Clang 的参数
。
当我们构建 Foundation 模块时,Foundation 模块也包含了其他框架,所以我们也需要构建其他的模块。
![](https://img.haomeiwen.com/i1306450/aa5a346fd0193410.png)
我们也会发现,有些导入是相同的,所以我们可以开始重用这些模块。
所有的这些模块都可以被缓存在磁盘中的模块缓存中。
当创建模块时,所有命令行参数都会被传递过来,而这些参数可以影响模块的最终内容。
所以,我们需要对这些参数进行哈希
,然后针对这一次编译调用,把模块存储到与这个哈希值
对应的文件夹中。
![](https://img.haomeiwen.com/i1306450/be6506dec09bcd3d.png)
如果你之后你改变了编译器的一些参数,这时就会有一个新的哈希值
产生。
这时就需要 Clang 重新构建所有输入,并存储到与新的哈希值对应的文件夹中。
![](https://img.haomeiwen.com/i1306450/dc65918f304c0691.png)
所以,为了尽可能地重用这些模块缓存,你要尽可能地保持参数相同。
以上就是我们如何为系统库构建模块。
那么,我们如何为开发者的框架构建模块呢?
让我们回到 Cat 头文件的例子,这一次我们会启用模块。
![](https://img.haomeiwen.com/i1306450/2a38b897c65cd744.png)
如果我们使用头文件映射,头文件映射会指向源代码目录。
但是,这就产生了问题,因为源代码目录不是模块目录。
![](https://img.haomeiwen.com/i1306450/5785ff0538b6d90a.png)
这时候,Clang 不知道该如何处理这种情况。
为了解决这个问题,我们引入了一个新的概念—— Clang 的虚拟文件系统(Clang's Virtual File System)
。
![](https://img.haomeiwen.com/i1306450/ddf1b58b28e3ac72.png)
它会创建一个框架的虚拟抽象,然后让 Clang 可以构建这个模块。
而这个抽象基本上又指向源代码目录的文件。
![](https://img.haomeiwen.com/i1306450/f38771ab48bf94e0.png)
因此,Clang 就可以为你的源代码生成警告和错误。
这就是我们如何为开发者的框架构建模块的相关内容。
最后还有一个提醒,如果你不指定框架的名称,你将会遭遇一些坑!
让我们来看一个简单的例子。
![](https://img.haomeiwen.com/i1306450/c3b04dde955cc3d3.png)
这里有两个导入操作,第一个导入 PetKit 模块。
对于第二个导入,虽然我们知道这是 PetKit 模块的一部分,但是 Clang 并不知道,因为你没有指定框架的名称。
在这种情况下,你可能会遭遇重复定义的错误,这种错误基本上是在你重复导入同一个头文件时发生。
只需要做一个微小的调整,Clang 就可以成功为你的框架构建模块。
![](https://img.haomeiwen.com/i1306450/f97bfd24aa7e0d43.png)
所以,建议在导入框架时指定框架的名称,无论这个框架是公开的还是你自己私有的。
继续阅读 (WWDC) Xcode 构建过程的幕后 —— Swift 。
参考内容:
Behind the Scenes of the Xcode Build Process
转载请注明出处,谢谢~
网友评论