理解 Xcode 中的各种概念

Xcode 有非常多的概念,比如:workspace、project、target、product、scheme 等,这些概念之间有着千丝万缕的关系,当我们理解了这些概念及其关系之后,会对整个 Xcode 工程体系有一个整体的理解,对我们自身工程能力的提升也会有所帮助。本文将对这些概念及其关系进行梳理,从而为后续的学习提供基础。

整体关系

下图所示是 Xcode 的核心概念之间的关系示意图,我们简单地进行介绍一下。

  • Workspace:Xcode 提供的一个工作空间,其可以包含多个 project。通过组合 project,我们可以实现非常庞大、复杂的工程。
  • Project:一个 Xcode 工程的核心,project 维护并管理源代码、资源文件、框架和库等。此外,project 还提供了默认的 build configurations,用于指导构建 product。其中,每个 build configuration 包含一组 build setting
  • Target:其包含构建特定 product 所需的 build configuration、build rule、build phase,并指定对应的构建产物 product。
  • Product:基于 project 的源文件,根据 build phase、build rule 以及 project 和 target 的 build configuration 所构建的产物。

下面,我们分别介绍一下这几个核心概念。

Workspace

Xcode Workspace 主要用于为 Xcode project 提供一个工作空间,通过组合 project 可以实现一个复杂的工程。

一个 workspace 可以包含任意数量的 project,以及任意其他要处理的文件Xcode 能够记录或推导这些 project 和 target 之间显式或隐式的依赖关系

  • 对于隐式依赖,比如,在同一个 workspace 中,project A 构建了一个库,该库又被 project B 链接,那么 Xcode 会在构建 project B 之前,自动构建 project A(即使 build setting 中并没有明确此依赖关系)。
  • 对于显式依赖,我们必须创建 project 进行引用。

Workspace 也是 Xcode 工作流的一种扩展。比如:文件的索引(indexing)是在 workspace 中进行的,因此代码补全、定义跳转以及其他内容感知的相关功能都可以在 workspace 下的所有 project 中使用。

默认情况下,workspace 中的所有 project 都在同一个目录下构建,即 workspace 构建目录。由于同一个 workspace 下的 project 的所有文件都在同一个构建目录下,所以该目录下的所有文件对于每一个 project 都是可见的。因此,如果有两个或多个 project 使用了相同的库,我们无需将它们复制到每个 project 中。如果某个 project 指定了构建目录,那么在构建该 project 时,该构建目录会被 project 所在的 workspace 的构建目录所覆盖。

同一个 workspace 下的每个 project 都是独立的。因此,为了不影响其他 project 或不受其他 project 影响,我们可以在不打开 workspace 的情况下直接打开 project,或者将 project 添加到另一个 workspace。由于 一个 project 可以同时属于多个 workspace,因此我们可以以任意组合 project,而无需重新配置任何 project 或 workspace。

Project

Xcode Project 是一个 Xcode 工程的核心部分,其本质上是一个文件、资源、构建信息所组成的仓库,能够构建一个或多个 product。一个 project 包含了所有用于构建 product 的元素,并且维护了这些元素之间的关系。Project 包含一个或多个 target,这些 target 各自定义了如何构建一个 product。一个 project 为其所包含的所有 target 定义了默认的 build configurations,其中的每个 target 又可以指定自己的 build configurations,target 定义的 build configurations 能够覆盖 project 定义的 build configurations。

Project 使用一个 .pbxproj 为后缀的文件进行描述,其主要包含了以下内容:

  • 对源文件的引用,如上图左侧导航栏所示。
    • 源代码,包含头文件和实现文件
    • 库和框架,内部或外部的
    • 资源文件
    • 图片文件
    • nil 文件
  • 用于在 Xcode 项目导航栏中组织源文件的组(Group)
  • Project 级别的 build configurations。如上图所示,我们会为一个 project 指定多个 build configuration,比如,我们会定义 Debug 和 Release 两种类型的 build configuration,并分别为它们定义 build settings,如上图所示。
  • 一个或多个 target,每个 target 均包含以下内容:
    • 对应 product 的引用
    • 构建 product 所需源文件的引用
    • 用于构建 product 的 build configurations
  • 用于调试或测试的可执行环境,其中每个可执行环境均包含以下内容:
    • 当通过 Xcode 运行或调试时,要启动哪个可执行文件
    • 当执行可执行文件时,要传递哪些命令行参数
    • 当程序运行时,要设置哪些环境变量

我们可以通过 scheme 来指定某一时刻哪个 target、哪个 build configuration 是有效的。

Target

Xcode Target 主要用于指导如何从 project 或 workspace 构建 product,其定义了构建系统的输入——源文件和处理源文件的配置,从而构建 product

如下所示,当我们通过 Xcode 创建 project 时,Xcode 会默认自动创建 target 用于指导构建主 app,此外 Xcode 会默认勾选 【include Tests】选项从而创建测试相关的 target,分别用于单元测试、UI 测试。

Target 中构建 product 的 build configurations 主要包括:build settings、build configuration 的名称(如:Debug)以及对 project 级别的 build configuration 的引用。

对于 build settings,我们应该都很熟悉,在日常开发中我们一般都通过 Xcode 项目编辑器直接进行编辑。Target 默认继承 project 定义的 build settings,当然,target 也可以通过自定义 build settings 进行覆盖。

一个 target 及其创建的 product 可以与另一个 target 产生关联。如果 target A 需要 target B 的输出才能构建,即 target A 依赖了 target B。如果两个 target 在同一个 workspace 中,那么 Xcode 就能够找到其中的依赖关系,在这种场景下,Xcode 会按顺序构建 product。这种关系称为隐式依赖。

我们还可以在 build settings 中显式地指定 target 的依赖项,并且我们可以将 Xcode 认为具有隐式依赖关系的两个 target 指定为不相关。比如:我们可以在同一个 workspace 中同时构建一个库和一个链接了这个库的应用程序。但是,如果我们想要链接到某个版本的库,而 workspace 中构建的这个库并不是这个版本,那么我们可以在 build settings 中创建一个显式依赖,它将覆盖隐式依赖。

同一时刻只能有一个活跃的 target,由 scheme 指定。

Product

Xcode Product 是基于 project 或 workspace 提供的源文件,根据 project 和 target 的 build configuration 构建而成的产物。苹果预定义了几种 Product 类型,主要有:应用程序(application)、测试(test)、静态库(static library)、框架(framework)等。这些 Product 类型,可以从文件的后缀进行区分。

Build Settings

Xcode Build Settings 是一组变量,包含有关 product 构建过程的各种信息。比如:build settings 中的信息可以指定 Xcode 传递哪些选项给编译器。

我们可以在 project 级别或 target 级别设置 build settings。每个 project 级别的 build setting 都会应用到 project 中的所有 target,除非 target 显式地覆盖了 build setting。

每个 target 组织构建一个 product 所需的所有源文件。Build Configuration 则指定了一组 build settings,从而以特定方式为 target 构建 product。比如:对于构建 Debug 和 Release 类型的 product,我们会分别定义两个 build configuration,每个 build configuration 各自包含一组 build settings。

Xcode 中的 build setting 有两个部分:

  • setting title:标识了 build setting,并且可以被其他 setting 所使用。
  • setting definition:一个常量或公式,Xcode 在构建时会用它来确定 build setting 的值。

Build setting 也可能有一个显示名称,用于在 Xcode 用户界面中显示 build setting。如下所示就是 Xcode 中的一些 build setting。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
ALWAYS_SEARCH_USER_PATHS: 'NO'
CLANG_ANALYZER_NONNULL: 'YES'
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION: YES_AGGRESSIVE
CLANG_CXX_LANGUAGE_STANDARD: gnu++14
CLANG_CXX_LIBRARY: libc++
CLANG_ENABLE_MODULES: 'YES'
CLANG_ENABLE_OBJC_ARC: 'YES'
CLANG_ENABLE_OBJC_WEAK: 'YES'
GCC_WARN_UNUSED_FUNCTION: 'YES'
GCC_WARN_UNUSED_VARIABLE: 'YES'
IPHONEOS_DEPLOYMENT_TARGET: '10.0'
MTL_ENABLE_DEBUG_INFO: INCLUDE_SOURCE
MTL_FAST_MATH: 'YES'
ONLY_ACTIVE_ARCH: 'YES'
SDKROOT: iphoneos
SWIFT_ACTIVE_COMPILATION_CONDITIONS: DEBUG
SWIFT_OPTIMIZATION_LEVEL: "-Onone"
...

当我们使用模板创建一个新的 project 时,除了 Xcode 提供的默认的 build settings,我们还可以为 project 或 target 创建自定义的 build setting。我们还可以指定 条件式 build setting。条件式 build setting 的值取决于是否满足一个或多个先决条件。例如:我们可以根据 target 的架构决定是否使用某个 SDK。

Build Phases

Xcode Build Phases 是一系列在构建一个 target 期间执行的任务,如下图所示。

在 Xcode 中,主要包含 7 种类型的 build phase,分别如下:

  • Compile sources
    • 将可编译的源文件(如 Swift、Objective-C、Lex、Yacc)与当前 target 进行关联,并进行编译。如果有必要,为每个源文件指定编译选项。
    • 此种类型的 build phase 可以为每个 target 使用一次,但是对于 Aggregate Target 和 External Build Tool Target,此种类型的 build phase 无法使用
  • Copy bundle resources
    • 将资源与当前 target 进行关联,在合适时进行处理,并将它们复制到 product 的 Resource 子文件夹中。
    • 此种类型的 build phase 可以为每个 target 使用一次,并且当且仅当 target 的 product 支持嵌入式资源时才能使用
  • Copy files
    • 将其他 target 的 product 与当前 target 进行关联,必要时对他们进行 code sign,然后将它们复制到目标地址(一般是 product 中的一个子文件夹)。
    • 此种类型的 build phase 在所有构建过程或进行安装构建过程中使用,每个 target 可以多次使用它
  • Headers
    • 将 public header、private header、project header 文件与当前 target 进行关联。public/private header 定义对外的 API,并且会被复制到 product 中进行安装。比如:一个 framework target 中的 public/private header 会被复制到 product 的 Headers 和 PrivateHeaders 子文件夹中。project header 定义 target 使用的 API,但不会复制到 product 中。
    • 每个 target 只能使用一次此种类型的 build phase
  • Link binary with libraries
    • 将 Apple frameworks 等库与当前 target 进行关联。这些库可以是平台库、其他 target 生成的库、外部预构建的 XCFramework 和库。这些库可以被指定为必选的或可选的(弱链接——即库不存在,应用程序也可以加载)。
    • 此种类型的 build phase 可以为每个 target 使用一次,但是对于 Aggregate Target 和 External Build Tool Target,此种类型的 build phase 无法使用
  • Run script
    • 在构建过程中运行指定的 shell 脚本。此脚本可以使用当前 target 的 build settings 所定义的变量,比如:$(SRCROOT),表示包含目标源文件的目录。此外,还允许我们提供输入和输出文件的列表。当没有提供输入和输出文件时,脚本也会运行。当提供输入和输出文件时,脚本仅在之前从未运行、或输入文件发生过变化、或输出文件发生过丢失时运行。
    • 此种类型的 build phase 在所有构建过程或进行安装构建过程中使用,每个 target 可以多次使用它
  • Target dependencies
    • 显式指定在同一 project(非同一 workspace)或链接 project 中的其他 target,这些 target 必须在当前 target 自身构建之前进行构建。比如:依赖于 project 中另一个框架的 target 通常会配置有对该框架的 target 依赖项。Xcode 能够隐式地推断出一些依赖项,但是此 build phase 能够提供对依赖项及其构建顺序的更优的控制。

Scheme

Xcode Scheme 定义一组操作,默认有:Build、Test、Launch、Profile、Analyze、Archive。每一种操作定义了一系列的指令,包括:target、build configuration、arguments、options 等等,这些参数、指令共同构成一个构建方案,从而用于构建一个或多个 target。

我们可以拥有任意数量的 scheme,但一次只能激活一个 scheme,对应在 Xcode 的右上角我们每次只能选中一个 scheme。

总结

本文简单梳理了一下 Workspace、Project、Target、Product、Scheme 等概念,对 Xcode 的整个构建体系有了一个大致的认识。

后续我们再来详细介绍一下这些概念在 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. Let's Talk About project.pbxproj
  6. Xcode Project File Format
  7. Xcode 编辑器之Workspace,Project,Scheme,Target
  8. iOS Xcode工程目录的 folder 和 group的区别
  9. Configure schemes
  10. Xcodeproj
  11. What are build phases?