本文由 简悦 SimpRead 转码, 原文地址 www.objc.io
Objc.io 出版有关 iOS 和 macOS 开发高级技术的书籍、视频和文章。
如今,我们有点被宠坏了--我们只需在 Xcode 中点击一个按钮,看起来应该是播放一些音乐,几秒钟后,我们的应用程序就开始运行了。这简直太神奇了。直到出错。
在本文中,我们将对构建过程进行一次高层次的参观,并了解这一切是如何与 Xcode 在其界面中提供的项目设置相联系的。如果您想更深入地了解每个步骤的实际工作原理,请参阅本期的其他文章。
解密构建日志
要了解 Xcode 构建过程的内部运作,我们首先要做的就是查看完整的日志文件。打开 "日志导航器",从列表中选择一个构建,然后 Xcode 将以一种经过修饰的格式向您显示日志文件。
默认情况下,该视图隐藏了大量信息,但您可以通过选择每个任务并点击右侧的展开按钮来显示其详细信息。另一个方法是从列表中选择一个或多个任务,然后点击 Cmd-C。这将把纯文本全文复制到剪贴板。最后但并非最不重要的一点是,你还可以从编辑器菜单中选择 "复制副本以显示结果",将完整的日志转储到剪贴板中。
在我们的示例中,日志长度接近 10,000 行(当然,最大的部分来自编译 OpenSSL,而不是我们自己的代码)。那么,让我们开始吧!
首先,你会发现日志输出被分成几大块,与项目中的目标相对应:
Build target Pods-SSZipArchive
...
Build target Makefile-openssl
...
Build target Pods-AFNetworking
...
Build target crypto
...
Build target Pods
...
Build target ssl
...
Build target objcio
我们的项目有几个依赖项: AFNetworking 和 SSZipArchive 作为 Pod,OpenSSL 作为子项目。
对于每一个目标,Xcode 都会经过一系列步骤,将源代码实际翻译成所选平台的机器可读二进制文件。让我们仔细看看第一个目标:SSZipArchive。
在该目标的日志输出中,我们可以看到沿途执行的每项任务的详细信息。例如,第一个是处理预编译头文件的日志(为了让它更易读,我去掉了很多细节):
(1) ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7 objective-c com.apple.compilers.llvm.clang.1_0.compiler
(2) cd /.../Dev/objcio/Pods
setenv LANG en_US.US-ASCII
setenv PATH "..."
(3) /.../Xcode.app/.../clang
(4) -x objective-c-header
(5) -arch armv7
... configuration and warning flags ...
(6) -DDEBUG=1 -DCOCOAPODS=1
... include paths and more ...
(7) -c
(8) /.../Pods-SSZipArchive-prefix.pch
(9) -o /.../Pods-SSZipArchive-prefix.pch.pch
在构建过程中,每个任务都会出现这些块,让我们来详细了解一下这个任务。
-
每个块都以一行描述任务的文字开始。
-
接下来的缩进行列出了该任务要执行的语句。在本例中,工作目录被更改,
LANG和PATH环境变量被设置。 -
有趣的事情就在这里发生。为了处理
.pch文件,clang 会调用大量选项。这一行显示了包含所有参数的完整调用过程。让我们来看看其中的几个... -
x "标志指定了语言,在本例中是 "objective-c-header"。
-
目标架构指定为
armv7。 -
添加隐式
#define。 -
c
标志告诉 clang 它应该做什么。c表示运行预处理器、解析器、类型检查、LLVM 生成和优化,以及目标特定的汇编代码生成阶段。最后,它意味着运行汇编器本身,生成一个.o对象文件。 -
输入文件。
-
输出文件。
要做的事情有很多,我们将不对每项可能的任务进行详细说明。重点是你可以全面了解在构建过程中,哪些工具会被调用,以及在幕后使用了哪些参数。
对于该目标,虽然只有一个 .pch 文件,但实际上有两个任务要处理 objective-c-header 文件。仔细观察一下这些任务,我们就知道发生了什么:
ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7 objective-c ...
ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7s objective-c ...
目标构建有两种架构--armv7 和 armv7s,因此 clang 需要处理两次文件,每种架构一次。
在处理预编译头文件的任务之后,我们发现 SSZipArchive 目标的其他任务类型:
CompileC ...
Libtool ...
CreateUniversalBinary ...
这些名称几乎不言自明:"CompileC "编译".m "和".c "文件,"Libtool "从对象文件创建一个库,而 "CreateUniversalBinary "任务最终将上一阶段的两个".a "文件(每个架构一个)合并为一个通用二进制文件,可在 armv7 和 armv7s 上运行。
随后,我们项目中的所有其他依赖项也会执行类似的步骤。AFNetworking 将作为 pod 库与 SSZipArchive 一起编译和链接。构建 OpenSSL,处理 crypto 和 ssl 目标。
在准备好所有这些依赖项之后,我们终于找到了应用程序的目标。该目标的日志输出除了我们在编译库时已经看到的任务外,还包括一些其他有趣的任务:
PhaseScriptExecution ...
DataModelVersionCompile ...
Ld ...
GenerateDSYMFile ...
CopyStringsFile ...
CpResource ...
CopyPNGFile ...
CompileAssetCatalog ...
ProcessInfoPlistFile ...
ProcessProductPackaging /.../some-hash.mobileprovision ...
ProcessProductPackaging objcio/objcio.entitlements ...
CodeSign ...
在此列表中,唯一没有不言自明的名称的任务可能是 Ld,它是链接器工具的名称。它与 libtool 非常相似。事实上,libtool只是调用ld和lipo。ld 用于创建可执行文件,libtool 用于创建库。有关编译和链接工作原理的详细信息,请参阅 Daniel 和 Chris 的文章。
这些步骤中的每一步都会调用命令行工具来完成实际工作,就像我们在上面的 "ProcessPCH "步骤中看到的那样。不过,我们将从另一个角度来探讨这些任务,而不是让您继续翻阅日志文件: Xcode 如何知道哪些任务必须执行?
控制构建过程
当您在 Xcode 5 中选择一个项目时,项目编辑器顶部会显示六个选项卡: 常规"、"能力"、"信息"、"构建设置"、"构建阶段 "和 "构建规则"。
对于我们理解构建过程而言,后三个选项卡最为重要。
构建阶段
构建阶段代表了如何从代码到可执行二进制文件的高层次计划。它们描述了一路上必须执行的各种任务。
首先,建立目标依赖关系。它们告诉联编系统,在开始联编当前目标之前,必须先联编哪些目标。这不是一个 "真正的 "构建阶段。Xcode 只是将 GUI 与构建阶段一起呈现。
在 CocoaPods 特定的 script execution 构建阶段之后(有关 CocoaPods 和构建过程的更多信息,请参阅 Michele 的文章),"编译源代码 "部分指定了所有需要编译的文件。请注意,这里并没有说明这些文件的编译方式。我们将在了解编译规则和编译设置时进一步了解这方面的内容。本节中的文件将根据这些规则和设置进行处理。
编译完成后,下一步就是将所有文件连接到一起。看,这就是 Xcode 中列出的下一个构建阶段:"将二进制文件与库链接"。该部分列出了所有静态和动态库,这些库将与上一步编译生成的对象文件链接。静态库和动态库的处理方式有很大不同,更多详情请参考 Daniel 有关 Mach-O 可执行文件 的文章。
链接完成后,最后一个构建阶段是将图像和字体等静态资源复制到应用程序捆绑包中。实际上,PNG 图像不仅会被复制到目的地,还会在途中被优化(如果您在构建设置中开启了 PNG 优化)。
虽然复制静态资源是最后一个构建阶段,但构建过程尚未完成。例如,代码签名仍需进行,但这不属于构建阶段;它属于最后的构建步骤 "打包"。
自定义构建阶段
如果默认设置无法满足您的需求,您可以完全控制这些构建阶段。例如,你可以添加运行自定义脚本的构建阶段,CocoaPods 使用这些脚本可以完成额外的工作。你还可以添加额外的构建阶段来复制资源。如果你想将某些资源复制到特定的目标目录中,这将非常有用。
自定义构建阶段的另一个妙用是在应用程序图标上标注版本号和提交哈希值。为此,您可以添加一个 "运行脚本 "构建阶段,使用以下命令获取版本号和提交哈希值:
version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"`
commit=`git rev-parse --short HEAD`
之后,就可以使用 ImageMagick 修改应用程序图标了。有关如何操作的完整示例,请查看 this GitHub project 。
如果你想鼓励自己或同事保持源文件简洁,可以添加一个 "运行脚本 "构建阶段,如果源文件超过一定大小(本例中为 200 行),就会发出警告。
find "${SRCROOT}" \( -name "*.h" -or -name "*.m" \) -print0 | xargs -0 wc -l | awk '$1 > 200 && $2 != "total" { print $2 ":1: warning: file more than 200 lines" }'
编译规则
编译规则指定了不同文件类型的编译方式。通常情况下,你不需要修改这里的任何内容,但如果你想为某种文件类型添加自定义处理,你只需添加一条新的编译规则即可。
编译规则指定了适用的文件类型、处理文件的方式和输出的位置。比方说,我们创建了一个预处理器,它将基本的 Objective-C 实现文件作为输入,解析该文件中的注释以使用我们创建的语言生成布局约束,并输出一个包含生成代码的 .m 文件。由于我们无法拥有一个将 .m 文件作为输入和输出的构建规则,因此我们将使用扩展名 .mal,并为此添加一个自定义构建规则:
该规则规定,它适用于所有匹配*.mal的文件,并且这些文件应使用自定义脚本进行处理(该脚本以输入和输出路径为参数调用我们的预处理器)。最后,该规则告诉编译系统在哪里可以找到该编译规则的输出。
在这种情况下,由于输出只是一个普通的 .m 文件,因此它将被用于编译 .m 文件的编译规则接收,一切都将按照我们手动将预处理步骤的结果写成 .m 文件的方式进行。
在脚本中,我们使用一些变量来指定正确的路径和文件名。你可以在 Apple 的 Build Setting Reference 中找到所有可用变量的列表。要在构建过程中查看所有现有环境变量的值,可以添加 "运行脚本 "构建阶段,并选中 "在构建日志中显示环境变量 "选项。
构建设置
到目前为止,我们已经了解了构建阶段如何用于定义构建过程中的步骤,以及构建规则如何指定编译过程中应如何处理每种文件类型。在联编设置中,你可以配置联编日志输出中的每项任务的执行细节。
你会发现从编译、链接到代码签名和打包,构建过程的每个阶段都有大量选项。请注意这些设置是如何划分成不同部分的,这些部分大致与编译阶段相关,有时还与编译的特定文件类型相关。
其中许多选项都有相当不错的文档,你可以在右侧的快速帮助检查器或构建设置参考中查看。
项目文件
除了其他项目相关信息(如文件组)外,我们上面讨论的所有设置都会保存到项目文件(.pbxproj)中。在发生合并冲突之前,你很少会接触到这个文件的内部结构。
我鼓励你用自己喜欢的文本编辑器打开项目文件,从上到下浏览一遍。它的可读性出乎意料地好,你可以很容易地理解大部分章节的意思。阅读并理解这样一个完整的项目文件,会让合并冲突变得不那么可怕。
首先,我们查找名为 rootObject 的条目。在我们的项目文件中,可以看到以下一行:
rootObject = 1793817C17A9421F0078255E /* Project object */;
从这里,我们只需按照这个对象的 ID (1793817C17A9421F0078255E),找到我们的主项目定义:
/* Begin PBXProject section */
1793817C17A9421F0078255E /* Project object */ = {
isa = PBXProject;
...
该部分包含几个关键字,我们可以根据这些关键字进一步了解该文件是如何构建的。例如,mainGroup 指向根文件组。如果你遵循这一参考,你将很快了解项目结构是如何在 .pbxproj 文件中表示的。不过,让我们来看看与构建过程有关的内容。target "键指向构建目标定义:
targets = (
1793818317A9421F0078255E /* objcio */,
170E83CE17ABF256006E716E /* objcio Tests */,
);
在第一个引用之后,我们找到了目标定义:
1793818317A9421F0078255E /* objcio */ = {
isa = PBXNativeTarget;
buildConfigurationList = 179381B617A9421F0078255E /* Build configuration list for PBXNativeTarget "objcio" */;
buildPhases = (
F3EB8576A1C24900A8F9CBB6 /* Check Pods Manifest.lock */,
1793818017A9421F0078255E /* Sources */,
1793818117A9421F0078255E /* Frameworks */,
1793818217A9421F0078255E /* Resources */,
FF25BB7F4B7D4F87AC7A4265 /* Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
1769BED917CA8239008B6F5D /* PBXTargetDependency */,
1769BED717CA8236008B6F5D /* PBXTargetDependency */,
);
name = objcio;
productName = objcio;
productReference = 1793818417A9421F0078255E /* objcio.app */;
productType = "com.apple.product-type.application";
};
buildConfigurationList 指向可用的配置,通常是 "调试 "和 "发布"。在调试引用之后,我们最终找到了构建设置选项卡中所有选项的存储位置:
179381B717A9421F0078255E /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 05D234D6F5E146E9937E8997 /* Pods.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = YES;
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
CODE_SIGN_ENTITLEMENTS = objcio/objcio.entitlements;
...
buildPhases 属性简单地列出了我们在 Xcode 中定义的所有构建阶段。幸运的是,Xcode 在 C 风格注释中用对象的真实名称增强了对象的 ID,因此很容易识别它们。buildRules "属性为空,因为我们没有在此项目中定义任何自定义构建规则。依赖项 "列出了在 Xcode 的 "构建阶段 "选项卡中定义的目标依赖项。
没那么可怕吧?我将把它作为一个练习,让您查看项目文件的其余部分。只要跟着对象 ID 就可以了。一旦您掌握了窍门,并理解了所有不同部分与 Xcode 中项目设置的关系,那么在出现更复杂的合并冲突时,找出出错的原因就变得非常容易了。您甚至可以开始读取 GitHub 上的项目文件,而无需克隆项目并在 Xcode 中打开它。
结论
现代软件建立在其他软件(如库和构建工具)的复杂堆栈之上。反过来,这些软件本身也是构建在更低层次的堆栈之上。这就像一层一层剥洋葱。虽然从整个堆栈一直到硅片都可能过于复杂,任何一个人都无法理解,但意识到你实际上可以剥开下一层,并理解那里发生了什么,就会非常有力量。没有什么魔法,只是一大堆层叠在一起,每一层都有基本相同的构建模块。
查看构建系统的引擎盖就是剥离其中的一层。我们不需要了解下面的整个堆栈,就能对按下运行按钮时发生的事情有所了解。我们只需再深入一层,就能发现调用其他工具的有序、可控的序列。我鼓励大家阅读本期的其他文章,了解洋葱的下一层!