前言
本文中的编译根据上下文有不同的含义,请注意区分。
- 编译原始概念是指:将 a.m 编译为 a.out
- 本文中,也可以用来表示根据项目产出构建产物(含“执行脚本”、“文件复制”等可选操作)
笔者每次进行 clang 工程编译时,都会被编译耗时困扰。 clang 每次编译都在5分钟左右。 首先,先提供一份效果对比图。
图1,如下所示,笔者在只改动 1 行代码时,编译速度耗时 300.1 秒。
图2,经过简单的处理,编译速度被优化到 28.6 秒。
Xcode 的编译构建
当我们执行点击 运行 按钮时,Xcode 会执行以下步骤:
- 执行 BuildAction ,为后续的 LaunchAction 做准备
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "NO"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A79DD8637E8944CF96F0A620"
BuildableName = "clang"
BlueprintName = "clang"
ReferencedContainer = "container:../../build/Xcode-DebugAssert/llvm-macosx-x86_64/LLVM.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
-
执行 LaunchAction,运行程序
<LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "A79DD8637E8944CF96F0A620" BuildableName = "clang" BlueprintName = "clang" ReferencedContainer = "container:../../build/Xcode-DebugAssert/llvm-macosx-x86_64/LLVM.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </LaunchAction>
Action 详解
下面会重点讲解两个 Action
的各种配置参数。 阅读本文可以只看LaunchAction 关键配置
和 BuildAction 关键配置
。LaunchAction 其它主要配置
和 BuildAction 其它主要配置
可以当做扩展阅读。
LaunchAction 关键配置
-
buildConfiguration
,代表执行 LaunchAction 使用的配置组合名。默认是Debug
-
通过 Xcode 新建的项目会有
Debug
和Release
两种配置组合。 -
Build Settings
中可以增加新的配置组合。如下,LLVM 共计有 4 种配置组合。 -
通过配置组合,开发者可以快速调整编译选项。如下,clang 在不同的配置组合下,开启了不同的编译优化级别
-
BuildableProductRunnable
编译配置。 -
ReferencedContainer
代表依赖的文件位置 -
BlueprintIdentifier
代表ReferencedContainer
的节点ID。A79DD8637E8944CF96F0A620
的内容会在下一节重点讲解(后面会用clang-target
代替)<BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "A79DD8637E8944CF96F0A620" BuildableName = "clang" BlueprintName = "clang" ReferencedContainer = "container:../../build/Xcode-DebugAssert/llvm-macosx-x86_64/LLVM.xcodeproj"> </BuildableReference> </BuildableProductRunnable>
LaunchAction 其它主要配置
-
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
代表使用的调试器。如果不希望调试程序,可以通过置为空,避免启动调试器。selectedDebuggerIdentifier = ""
-
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
代表使用的启动器。如果不希望调试程序,可以改为 Spawn 模式。 selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
-
debugAsWhichUser
代表是否使用 root 用户执行程序。 -
allowLocationSimulation
是否模拟自己的所在位置。通常用于开发 APP 进行位置模拟时使用。 -
enableAddressSanitizer = "YES"
是否启用地址杀毒剂。可以用于调试 EXC_BAD_ACCESS 等问题 -
enableASanStackUseAfterReturn = "YES"
-
enableUBSanitizer = "YES"
-
launchStyle = "1"
启动类型,1 代表需要程序员手动执行程序,0 代表自动执行程序 -
CommandLineArguments
启动APP时传递的参数-
EnvironmentVariables
环境变量,比如,可以通过以下命令控制动态库的动态加载 (看过 调试 iOS 的 objc 运行时,你可能还需要掌握这些知识 的同学,可以研究一下这个环境变量)<EnvironmentVariables> <EnvironmentVariable key = "DYLD_INSERT_LIBRARIES" value = "A_DYLD_INSERT_LIBRARIES" isEnabled = "YES"> </EnvironmentVariable> </EnvironmentVariables>
-
BuildAction 的关键配置
parallelizeBuildables = "YES"
执行编译任务时,是否多任务并行处理buildImplicitDependencies = "NO"
是否查找隐式依赖
BuildAction 的其它重要配置
BuildActionEntry
执行其它action时,是否触发BuildAction
。buildForTesting = "NO"
代表,执行单测任务时,不需要触发编译buildForRunning = "YES"
代表,执行 运行任务时,需要触发编译任务
project.pbxproj 的 clang-target
配置
在 LaunchAction 中,我们曾经提到 clang-target
(A79DD8637E8944CF96F0A620
)。 因这部分的配置是在 project.pbxproj 中维护,所以单独用一个小节讲解一下。 如下所示,project.pbxproj 项目中关于 clang-target
)的配置(因为篇幅原因,dependencies 被省略了一部分)
A79DD8637E8944CF96F0A620 /* clang */ = {
isa = PBXNativeTarget;
buildConfigurationList = A7BAD0EC9F7549319D37DAB4 /* Build configuration list for PBXNativeTarget "clang" */;
buildPhases = (
8720B378A9BF485883000F4C /* Sources */,
05FD7D0438324244B5AB2823 /* CMake PostBuild Rules */,
);
buildRules = (
);
dependencies = (
5D887B4D650A443FAD81013A /* PBXTargetDependency */,
........
........
........
);
name = clang;
productName = clang;
productReference = 66B22582EC2946B9B97BCED6 /* clang */;
productType = "com.apple.product-type.tool";
};
-
isa
就像 OC 中的类有自己的 isa 属性一样,这里同样有isa
标明这是一个 targetPBXNativeTarget
-
buildConfigurationList
指向一个列表,该列表负责维护clang-target
有几种配置组合A7BAD0EC9F7549319D37DAB4 /* Build configuration list for PBXNativeTarget "clang" */ = { isa = XCConfigurationList; buildConfigurations = ( 4FF7FFFB5E8D4AFAB08BDF78 /* Debug */, 895C7D31C2AE46899583672D /* Release */, 85E6509B95FE4402A01A4657 /* MinSizeRel */, 1BAC7DB5A2ED41A0A31AB96E /* RelWithDebInfo */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; };
-
buildPhases
负责维护一个编译 phase 列表PBXSourcesBuildPhase
源码编译:指向clang-target
的编译代码
注意:这里只有
cpp
c
m
等文件,没有h
等头文件8720B378A9BF485883000F4C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F91A7D5B43464D99B6C2B745 /* cc1_main.cpp in Sources */, 77711346965C4D53B41045FE /* cc1as_main.cpp in Sources */, FCA5E4060CCE4B9FBE948241 /* cc1gen_reproducer_main.cpp in Sources */, 98295C12ADE0429190E290C0 /* driver.cpp in Sources */, 5C039FA62D1A42F2946FBDC0 /* dummy.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };
-
PBXShellScriptBuildPhase
脚本执行。(执行效果是通过创建链接的方式,将clang++
clang-cl
clang-cpp
指向clang-target
的构建产物clang
)
05FD7D0438324244B5AB2823 /* CMake PostBuild Rules */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
name = "CMake PostBuild Rules";
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "make -C ~/swift-source/build/Xcode-DebugAssert/llvm-macosx-x86_64/tools/clang/tools/driver -f ~/swift-source/build/Xcode-DebugAssert/llvm-macosx-x86_64/tools/clang/tools/driver/CMakeScripts/clang_postBuildPhase.make$CONFIGURATION OBJDIR=$(basename \"$OBJECT_FILE_DIR_normal\") all";
showEnvVarsInLog = 0;
};
-
dependencies
负责维护的依赖列表(可以近似理解为对 静态库 或 动态库 的依赖)。clang-target
共计有 108 个依赖。- 如下所示,这个依赖表明只有
LLVMDemangle
完成构建后,clang-target
才能执行完毕
- 如下所示,这个依赖表明只有
5D887B4D650A443FAD81013A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4F1F4485C1C44E48A8AF56BB /* LLVMDemangle */;
targetProxy = 2A241593F41943B38ACDA422 /* PBXContainerItemProxy */;
};
-
name
代表target的名字。 -
productName
代表该 target 产物的名字。-
如下,左侧是
name
,右侧是productName
-
-
productReference
代表产物的ID -
productType
代表产物的类型。字段的含义可以参考/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Specifications/MacOSX Product Types.xcspec
- clang的产物类型是
com.apple.product-type.tool
,代表 普通 Unix 命令行可执行文件
- clang的产物类型是
// Tool (normal Unix command-line executable)
{ Type = ProductType;
Identifier = com.apple.product-type.tool;
Class = PBXToolProductType;
Name = "Command-line Tool";
Description = "Standalone command-line tool";
IconNamePrefix = "TargetExecutable";
DefaultTargetName = "Command-line Tool";
DefaultBuildProperties = {
FULL_PRODUCT_NAME = "$(EXECUTABLE_NAME)";
MACH_O_TYPE = "mh_execute";
EXECUTABLE_PREFIX = "";
EXECUTABLE_SUFFIX = "";
REZ_EXECUTABLE = YES;
INSTALL_PATH = "/usr/local/bin";
FRAMEWORK_FLAG_PREFIX = "-framework";
LIBRARY_FLAG_PREFIX = "-l";
LIBRARY_FLAG_NOSPACE = YES;
GCC_DYNAMIC_NO_PIC = NO;
GCC_SYMBOLS_PRIVATE_EXTERN = YES;
GCC_INLINES_ARE_PRIVATE_EXTERN = YES;
STRIP_STYLE = "all";
CODE_SIGNING_ALLOWED = YES;
};
PackageTypes = (
com.apple.package-type.mach-o-executable // default
);
WantsSigningEditing = YES;
WantsBundleIdentifierEditing = YES;
},
优化原理
当我们执行点击 运行 按钮时,Xcode 会执行以下步骤:
- 读取各类配置信息
- 根据
LaunchAction
确定配置组合,本例是Debug
- 根据
LaunchAction
确定编译 target,本例是clang-target
- 根据
BuildAction
确定是否查找隐式依赖 - 根据
BuildAction
确定是否并行编译 - 根据 编译 target 确定前置依赖的
dependencies
,递归执行本步骤 - 根据 依赖图
dependencies
确定编译任务
- 根据
- 并发或者顺序执行编译步骤
- 预处理
- 编译
- 链接
- 对产物签名
- 执行脚本
实际上,上面构建 依赖图的根本原因是:编译 clang 需要其它 target 的 些构建产物才能完成。 重点:在增量编译场景下,大部分的产物实际上是保持不变的,只有少数几个静态库会发生变化。
优化步骤
下面,我们看一下如何针对增量编译
场景特殊处理:
-
通过快捷键或者鼠标,复制一份新的 clang target,我们可以称之为 clang-mini
重点:复制的原因是,我们可以在某些场景下(比如不小心删除了某个文件),通过 clang 快速编译所有的依赖产物
-
更改 Product Name
重点:不是所有的优化都需要此步骤,clang 执行此步骤的原因是之前提到过,编译完成后,会有脚本创建clang++ 等快捷方式,所以,这里特定调整一下
-
保留会有源文件变更的依赖比如,我保留
LLVMDemangle
依赖的原因是,我需要加一下日志进行调试。 -
关闭隐式依赖的查找 重点:对于clang,这步的意义不大。但是,某些项目可能存在大量的隐式依赖,关掉这个开关,并且将依赖放到上一步的
Dependencies
中,可以节省大量的编译消耗
总结
本文通过分析 Xcode 运行构建工作的原理,提供了一种新的思路,可以快速降低大型项目的增量编译
的时间消耗。