Xcode构建过程的后台工作(一)构建过程
Xcode构建过程的后台工作(三)swift构建
Xcode构建过程的后台工作(四)链接
构建过程:Clang构建
什么是Clang
Clang是苹果官方编译器,用于所有的C语言,比如C,C++,OC,OC++,大部分框架都在用的语言。编译器一次编辑所有输入文件,生成仅一个输出文件,之后被连接器使用。如果要从OS访问API,或从自己的代码访问实现文件,就需要一个叫做头文件的东西。头文件是一种承诺,承诺在其他地方存在这个实现文件,它们通常可以匹配。如果你只是更新实现文件,而忘记头文件,你就食言了。通常这个问题不会在编译过程中出现,因为编译器相信你的承诺。问题出在链接过程中。编译器通常包含不止一个头文件,而且所有编译器都是这样被调用。
以demo PetWall为例。它是混合语言app,app本身用swift编写。它使用了一个用Objective-C编写的框架。 它有一个美学档案支持库,使用C++编写。 随着时间的推移,我们的应用程序越大,所以我们开始重组它,以便更容易查找文件。 例如,我们将所有与cat相关的文件移动到子文件夹中。我们不必更改任何实现文件,它仍然有效。所以它让你想知道Clang 如何找到你的头文件?让我们看一个简单的例子。
这是我们在代码中使用的实现文件之一,在这个文件里包含一个名为cat.h的头文件。 我们怎样才能弄清楚Clang的作用?一是查看构建日志,看看Xcode构建系统编译这个文件时做了什么。复制粘贴以下调用代码:
$ clane -c Cat.mm -o Cat.o -v
将其放入终端并输入-v,-v是详细符号。然后Clang会返回很多信息。 但是我们只需要关注一个重要的搜索路径。我说搜索路径,可能大家想到的是指向源代码的搜索路径。但不是这样的。相反,你会看到headermaps(头文件映射)。headermaps由Xcode构建系统创建,说明头文件的位置。
让我们来看看最重要的两个headermaps文件。 前两个条目只是 将框架名称附加到标题中。 这两个头文件原来是公共文件。
我建议你不应该依赖这个功能,原因是我们将其放在那里是为了保持现有项目的 正常运行,但是之后使用Clang模块可能会遇到问题,因此我们建议您在将公共或私有文件从自己的框架导入时自己标出框架名称。 第三行是项目头文件,这个例子中并不需要。 headermaps是为了链接回源代码。如您所见,公共和私有 头文件执行相同的操作,总是回归源代码。 这样做是为了让Clang可以为源目录中的文件生成有用的报错和警告消息, 而不是从构建目录中复制过来的其他内容。由于许多人不知道头文件映射的存在,因此会遇到某些问题。最常见的是忘了将头文件添加到项目中。 它位于源目录中, 但它不在项目中。 因此,请一定要保证将头文件添加到项目中。 另一个问题是,如果头文件具有相同的名称,则它们可能会相互影响。 因此,头文件务必使用唯一名称。这也适用于系统头文件。 如果 项目中里的本地头文件与系统头文件名字一样,它将覆盖系统头文件,因此应该避免这种情况。
如何找到系统头文件?
举PetWall的 另一个例子。 在这个例子中,我们引入了SDK中的Foundation.h头文件 。当我们寻找自己的头文件时,我们可以做同样的事情 。 但现在我们正在寻找系统头文件。 headermap只适用于您自己的头文件。所以我们可以忽略它们。 现在关注导入路径。默认的SDK中有两个目录。第一个是用户的,第二个是系统库框架。先来看看第一个。
这是一个常规的包含目录。我们只要输入搜索关键词,这里是Foundation/Foundation.h。头文件找不到,因为它不在那里。 不过没关系。 我们来试试下一个,系统库框架。
这是一个框架目录,所以着Clang的做法有些不同。首先,它要确定框架的定义,并检查框架是否存在。
之后,从头文件目录中查找头文件。
这里找到了,很好。 但是如果找不到头文件会发生什么? 例如输入不存在的虚假头文件。显然无法在 headers目录中找到它。但接下来它还会查看私有头文件目录。 Apple的SDK中不会带有任何私有头文件。但是您的项目和框架可能有公共和私有头文件。所以也会检查。
因为它是一个虚假的头文件,所以那里也没有。 现在有趣的是,会中止搜索。 我们不会继续搜索其他目录。原因是我们已经找到框架。找到框架后,一般框架目录中能找到头文件。如果没有找到,搜索就放弃了。 如果您对实现文件的样子很感兴趣,那么当所有头文件都被导入和预处理之后,您可以要求Xcode为您的实现文件创建预处理文件。
这将创建一个非常大的输出文件。有多大呢?举个简单的例子。Foundation.h是一个非常基础的头文件,是我们系统的基本头文件。它是您很可能直接或间接地为其他头文件导入此头文件。这意味着每次调用编译器,都要查找这个头文件 。一天之内Clang要为一个include语句查找并处理800多个头文件。也就是要解析和验证超过9兆字节的源代码。每次编译器调用都会发生这种情况。这是大量的冗余工作。怎么改善? 这里有一个功能称为预编译头文件。这是改善这种情况的一种方法。但我们有更好的东西。几年前,我们推出了Clang模块。Clang模块允许我们为每个框架只查找和解析一次头文件 ,然后将该信息存储在硬盘上,以便缓存和重复使用。这应该会改善您的构建时间。 为此,Clang模块必须具备特定的属性。其中最重要的一点是上下文无关(context-free)。什么是上下文无关?这里有两个代码片段:
在这两种情况下,我们都导入了 PetKit模块。但我们事先有两个不同的宏定义。 如果您使用传统方法导入头文件,则意味着文本也会被导入。预处理器将遵循此定义并将其应用于头文件。但是如果你这样做,那意味着每个案例的模块都不同,不能重用。因此,如果您想使用模块,则不能这样做。模块会忽略所有文本信息,这样就能被所有实现文件中重用。另一个要求是模块必须是独立的(self-contained)。也就是说要明确所有依赖关系。这有个好处,就是只要你导入一个模块,它就会起作用。不用考虑还要添加其他头文件才能运行。 那么Clang如何知道要不要构建一个模块呢?让我们看一下一个简单例子,NSString.h。
首先Clang要在框架中找到这个头文件。我们已经知道如何做到这一点。这是Foundation.framework目录。接下来,Clang编译器会查找模块目录和模块映射,它与头文件目录相关。
什么是模块映射?模块映射描述了某组头文件夹转换到模块中。模块图实际上非常简单。
这是Foundation的整个模块映射。它显然描述 了模块的名称,即 Foundation。然后它还指定了哪个 标头是该模块的一部分。
您会注意到这里只有一个头文件,只有 Foundation.h。但这是一个特殊的头文件。
这是umbrella头文件 ,用特殊关键字umbrella标记。这意味着Clang还要 查找这个特定的头文件,以确定NSString.h 是否是模块的一部分。
就在这里,NSString.h是Foundation模块的一部分。现在Clang可以将文本导入升级为模块导入,为此我们要创建Foundation模块。那么我们如何构建Foundation模块呢? 首先,我们要创建一个单独的Clang位置。Clang位置包含Foundation模块中的所有头文件。我们不会从原始编译器调用中转移任何现有上下文。因此,它是上下文无关的。我们实际转移的是您传递给Clang的命令行实参,随后继续传递。在构建Foundation模块时,框架本身会导入其他框架。 这意味着我们也必须构建这些模块。我们不能停顿,因为它可能还包括其他框架。但我们已经可以看到他的好处了。某些导入可能是相同的。所以能重用那个模块。
所有模块都要序列化到模块缓存区。
正如我所提到的,命令行实参会在创建该模块时向后传递。这意味着这些参数会影响模块的内容。因此,我们必须对这些参数进行哈希处理,并将为这些特定编译器调用而创建的模块存储在与该哈希匹配的目录中。如果更改不同限制文件的编译器参数,例如写入enable cat,这是不同的哈希,要求Clang重建所有模块到与该哈希匹配的目录中。
因此,为了更多地重用模块缓存,如果可能的话应该尝试保持参数一致性。
查找自己的头文件
我们如何为自己的头文件构建模块?回到原来的cat示例,这次我们打开模块。 如果我们要用头文件映射,则headermap会映射到源目录。
看一下这个源文件,现在遇到了问题。这里没有模块目录。它看起来根本不是框架 ,Clang在这种情况下不知所措。所以我们引入了一个新概念来解决这个问题,它被称为Clang的虚拟文件系统。它会创建一个虚拟的抽象框架,方便Clang构建模块。但抽象框架只能映射到目录文件。这样Clang就能够在源代码中报错。这就是在使用框架时构建模块的方式。
如您所知,在开始时我提到 如果不确定框架名称,可能会出现问题。那么让我举个例子看看是什么问题。
这是一个非常简单的代码示例,只有两个输入。
第一个导入PetKit模块。 第二次导入,我们都知道这也是PetKit模块的一部分,但Clang可能不知道,因为你没有指定框架名称。在这种情况下,您可能会 遇到重复定义的报错。这种情况常见于导入两次相同头文件。Clang在幕后非常努力地解决了这样的最常见问题。但它无法解决所有问题。这只是一个简单的例子。我们来做一点调整吧。
修改一下上下文。模块导入完全不受此影响,因为我说过的上下文可以忽略。cat导入仍然是头文件的文本导入,它会遵循此更改。这时可能就不是重复定义,而是矛盾定义,无法解决,Clang解决不了。记住我的建议,在导入公共或私有头文件时始终明确框架名称。