理解 Xcode 中的各种文件

上一篇文章我们介绍了 Xcode 中的各种概念,本文我们来看看这些概念在 Xcode 中的具体表示。其中,有一个最常见的文件 project.pbxproj,其描述了描述了整个 Xcode Project 的相关信息,包括:文件、Target、Product 等。另外,Xcode Workspace 则使用 contents.xcworkspacedata 进行描述,主要描述了其所包含的 Project 的位置。Xcode Scheme 则使用 .xcscheme 后缀的文件进行描述。

下面,我们来对这几个文件分别进行介绍。

project.pbxproj

Xcode Project 使用 project.pbxproj 来描述一个 Xcode 工程所包含的所有内容,包括文件、配置、库等,其存储在 Xcode 工程文件 *.xcodeproj 中。

文件格式

project.pbxproj 是一种旧风格的 Property List 文件(简称 plist)。Property List 与 JSON 最主要的区别在于数组和字典的表示:

  • 数组:使用小括号表示,数组元素使用逗号进行分隔
  • 字典:使用大括号表示,键和值之间使用等号链接
1
2
3
4
5
6
7
# plist 数组
("1", "2", "3")

# plist 字典
{
"key" = "value";
}

对象关系

本质上,project.pbxproj 是一个对象关系图,所有的对象都与 Xcode 中的某个文件、配置、操作相对应,每一个对象都使用一个 96 bit(24 位的十六进制)的UUID 进行唯一标识,如下所示。

1
2
3
00004989A1D96C5B52A8E09B8A3BC4B5 /* DoraemonFPSPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B3712CE0D0A78D8379015D4C5647631 /* DoraemonFPSPlugin.h */; settings = {ATTRIBUTES = (Project, ); }; };
00010094FB6FF27A828CF8A15DEAA184 /* TTCommonModelUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 740D1037D25E920AA9913B575B6AF7C0 /* TTCommonModelUtils.h */; settings = {ATTRIBUTES = (Project, ); }; };
0007199E29D1EB8473295A64FB7C1D99 /* CLIColor.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B0381C866EF001605422EEE7598F3CB /* CLIColor.h */; settings = {ATTRIBUTES = (Project, ); }; };

project.pbxproj 整体构成了一个树状的对象关系图,其类图大体如下所示。Xcode 中的各种概念大多数在类图中能够找到对应的类,比如:

  • Project 对应 PBXProject
  • Build Configuration 对应 XCBuildConfiguration
  • Target 对应 PBXTarget

Root Element

project.pbxproj Root Element 作为整个对象树的根节点。通过 rootObejct 为键引用一个 PBXProject 对象。objects 则定义了整个 project 中的所有相关文件。

PBXProject

PBXProject 定义了一个编译配置列表对象 buildConfigurationList,该对象中定义了一组 XCBuildConfiguration,其中每一个 XCBuildConfiguration 定义了一系列的 build settings。

关于文件的组织和维护,PBXProjectmainGroup 为键引用了一个 PBXGroup 对象。PBXGroup 对应 Xcode 中的 Group 的概念,类似于 Folder,但有所区别,主要用于 Xcode 中组织管理源文件。mainGroup 定义了文件组织结构的根节点,通过层层递进,可以索引到工程中的所有文件。

当然,PBXProject 还定义了一系列的 PBXTarget。这里的 PBXTarget 对应上述的 Xcode Target。

PBXTarget

类图中定义的 PBXTarget 是一个抽象类,其主要有以下几种具体类型:

  • PBXNativeTarget:一次构建一个 target。Xcode 项目中最常用的 target。
  • PBXLegacyTarget:通过命令行进行构建。如果一个 project 的依赖项需要通过 make 来构建,那么应该使用这种 target。
  • PBXAggregateTarget:同时构建多个 target,也可以用于执行不输出 product 的场景。

这些具体类型的 target,比如 PBXNativeTarget,引用了一系列的 PBXBuildRulePBXBuildPhase,还定义了输出的 product 的相关信息。

当然,这些 target 还引用了一组 XCBuildConfiugrationXCBuildConfiguration 中通过 baseConfigurationReference 引用了 project 级别的 XCBuildConfiguration,从而进行继承或覆盖。

PBXTarget 还定义了一组 PBXTargetDependency,即上述提到的如果 target A 依赖了 target B,那么 target B 就是 target A 的一个依赖项。

PBXBuildPhase

PBXBuildPhase 也是一个抽象类,其主要包含 7 种具体类型,正好与 Xcode 中的 7 种 build phase 相对应:

  • PBXHeadersBuildPhase:对应 Headers Phase
  • PBXSourcesBuildPhase:对应 Compile Sources Phase
  • PBXFrameworksBuildPhase:对应 Link Binary With Libraries Phase
  • PBXCopyFilesBuildPhase:对应 Copy Files Phase
  • PBXShellScriptBuildPhase:对应 Run Script Phase
  • PBXResourcesBuildPhase:对应 Copy Bundle Resources Phase
  • PBXRezBuildPhase:对应 Build Carbon Resources Phase

PBXBuildFile

project.pbxproj 使用 PBXBuildFile 表示构建文件,其定义了以下五种类型的文件引用。

  • PBXFileReference
  • PBXGroup
  • PBXReferenceProxy
  • PBXVariantGroup
  • XCVersionGroup

这里提到了 group 的概念,group 和 folder 是存在区别的,主要有以下两方面:

  • Group 只在工程一般是文件夹的形式,但是在本地的目录还是以散乱的形式放在一起,除非是从外部以 group 的形式引用进来,这种情况下的 group 同时是一个文件夹实体。
  • Folder 只能作为资源,folder 下的所有内容都会引入项目,不会被编译。也就是说,以 folder 形式引用进来的文件,不能被放在 compile sources 列表中。

contents.xcworkspacedata

Xcode Workspace 使用 contents.xcworkspacedata 来描述 workspace 中 project 的组成。如下所示,一个 workspace 包含两个 project,分别是:Demo.xcodeprojPods/Pods.xcodeproj

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Demo.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

.xcscheme

Xcode Scheme 使用 .xcscheme 后缀的 XML 文件进行描述,其存储在 xcshareddata 目录下。Xcode Scheme 默认定义了 6 种操作,如下图所示。

对应在 .xcscheme 文件中,同样定义了这 6 个默认 action,如下所示。每个操作中具体定义相关的 build configuration、arguments、options 等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0500"
version = "1.7">
<BuildAction>
...
</BuildAction>
<TestAction>
...
</TestAction>
<LaunchAction>
...
</LaunchAction>
<ProfileAction>
...
</ProfileAction>
<AnalyzeAction>
...
</AnalyzeAction>
<ArchiveAction>
...
</ArchiveAction>
</Scheme>

BuildableReference

.xcscheme 每一个 action 的依赖被定义在 BuildableReference 中。如果有一个 action 中包含了 BuildableReference,那么就意味着 Build Action 被作为当前 action 的依赖,首先被执行。在 .xcscheme 中,同一个 BuildableReference 可能会被包含多次,从而引用同一个 target。这里使用 UUID 来引用一个 target,由 BlueprintIdentifier 指定。这里的 UUID 对应的 target 定义在 project.pbxproj 文件中。如下所示,是一个 BuildableReference 的示例。

1
2
3
4
5
6
7
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3AA4FE2703EA4DC3A89C1CCC"
BuildableName = "libPods.a"
BlueprintName = "Pods"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>

Actions

Xcode Scheme 中的 action 中只有 Build Action 可以引用多个 target。因此,它在 BuildActionEntries 中包含了一个 BuildableReferences 列表,这些列表项引用了 target 所指定的显式依赖项。这些依赖项来源于 Target Dependencies 或 Link Binary With Library。对于其他的 action,它们会将 BuildableReference 嵌套在其内部,如果需要的话。

此外,每一个 action 都有这不同的配置选项,如下所示。所有的 action 都有 pre-actionspost-actions,它们可以在主 action 执行之前执行。相对来说,Analyze 和 Archive 的选项最少,Launch 的选项最多。

Options

所有的 action 都有相关的选项,它们会以属性的形式存储在 Action 中,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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 = "65851935276F4EFB000D11BE"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>

Pre-Actions & Post-Actions

Pre-Actions 和 Post-Action 主要有两种类型:

  • 发送邮件
  • 运行脚本

对于 Send Email Action,它会打开 Mail.app 并发送邮件,如下所示。

1
2
3
4
5
6
7
8
9
10
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.SendEmailAction">
<ActionContent
title = "Send Email"
emailRecipient = "thedoctor@tardis.space"
emailSubject = "Sup"
emailBody = "Bowties are cool"
attachLogToEmail = "NO">
</ActionContent>
</ExecutionAction>

对于 Script Action,它可以运行任何脚本。类似于 PBXShellScriptBuildPhase,脚本会被包含在 .xcscheme 文件中。默认,脚本会在构建目录下运行。因此,如果我们想修改 workspace 的内容,则可以运行 cd ${SRCROOT},如下所示。

1
2
3
cd ${SRCROOT}

pod install #>> /tmp/output.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "cd ${SRCROOT}&#10;&#10;pod install #&gt;&gt; /tmp/output.txt"
shellToInvoke = "/usr/bin/env bash">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D9B6428F176A2E17003D8169"
BuildableName = "Catstagrame.app"
BlueprintName = "Catstagrame"
ReferencedContainer = "container:Catstagrame.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>

总结

本文我们简单地介绍了一下 Xcode 中的三个文件:project.pbxprojcontents.xcworkspacedata.xcscheme。后两个文件分别描述了 workspace 和 scheme 的具体信息。project.pbxproj 则包含了除此之外的所有信息,包括:target、product、files、build configuration 等等。

了解了 Xcode 中的概念和文件之后,我们可以针对特定场景对这些文件进行修改、适配,从而解决特定问题。

参考

  1. Xcode Concepts
  2. The Project File Part 1: Composition
  3. The Project File Part 2: Schemes and Targets
  4. Xcode Project File Format
  5. Xcodeproj