Highlight
Xcode 16 把模块构建从编译器的隐式行为拆成三个显式阶段——scan、build modules、compile sources——让构建系统完全掌握模块依赖图,从而更高效地调度任务、避免执行通道被阻塞,并让调试器直接复用已编译的 Swift 模块。
核心内容
你有没有遇到过这种情况:同一个项目,clean build 和增量构建的时间差异巨大,而且增量构建的时间几乎不可预测?问题出在模块构建一直是编译器的隐式行为。每个编译任务遇到 import 时,自己去找模块、判断是否需要编译、如果别的任务正在编译就等着——整个协调过程对构建系统不可见。结果是执行通道被阻塞,你看到的编译任务时长参差不齐,开头的一批特别长,因为它们在等模块编译完成。更麻烦的是,调试器(LLDB)有自己独立的模块图,用 p 或 po 求值表达式时还得重新编译模块(08:03)。
Xcode 16 引入的 explicitly built modules 把这个隐式流程拆成了三个显式阶段:scan(扫描源文件构建模块依赖图)、build modules(编译模块)、compile sources(编译源文件)(06:01)。构建系统现在完全了解模块间的依赖关系,可以等模块就绪后再调度编译任务,避免执行通道被无效任务占满。对 C 和 Objective-C 代码,这是默认启用的;对 Swift,可以通过 Build Settings 中的 “Explicitly Built Modules” 开启(preview 阶段)(08:42)。调试器也能直接复用构建系统已编译好的 Swift 模块,不再需要单独重建(08:03)。
详细内容
模块的本质
模块是代码分发的单元,描述库或框架的接口。Swift 中,一个 target 的所有源文件组成一个模块,接口通过 public 等访问修饰符标记,编译器生成 .swiftinterface 和 .swiftmodule 文件。Objective-C 中,模块由头文件加 module map 描述,编译后生成 .pcm(precompiled module)文件(00:34)。
一个 module map 的结构如下:
framework module UIKit {
umbrella header "UIKit.h"
export *
module * { export * }
}
关键点:
framework module UIKit声明这是一个框架模块,名为 UIKit,@import UIKit就靠这个名字工作umbrella header "UIKit.h"指定包含所有头文件的伞头文件export *和module * { export * }表示该模块导入的其他模块也可以被导入方使用(01:46)
隐式 vs 显式模块构建
隐式构建下,编译器之间自行协调模块编译——谁先到谁编译,后来者等着。多个编译任务可能同时尝试编译同一个模块,只有一个能成功,其余的发现文件已存在就直接加载。这导致构建时间不可预测,构建日志也看不出模块编译占了多少时间(04:25)。
显式构建下,Xcode 把每个源文件的编译拆成三个阶段:
- Scan:扫描每个源文件,构建整个项目的模块依赖图,结果在构建日志中表现为 scan 任务。这是内置任务,不启动新进程,可以在源文件之间缓存信息(09:27)
- Build modules:根据模块图调度模块编译任务,每个模块启动独立的编译器进程。这些任务是顶层的,不附属于任何特定 target,因为模块可以跨 target 共享(09:47)
- Compile sources:编译源文件,但此时已经知道并注入了所需的已编译模块,编译时间大幅缩短(06:36)
模块变体(Module Variants)
同一个模块可能被编译多次——因为不同 target 的 build settings 不同,需要不同的模块变体。每个变体有一个哈希值,代表构建该变体所需的命令行参数集合(10:26)。常见的变体来源包括:
- 额外的预处理器宏(如
ENABLE_FEATURE) - 不同的语言模式(如 C 文件混在 Objective-C 项目中)
- 不同的语言版本(如 Objective-C 配合 C17)
- 关闭自动引用计数(ARC)(12:09)
你可以通过 Product > Perform Action > Build with Timing Summary 生成构建报告,然后在构建日志中搜索 “modules report” 查看 Clang 和 Swift 的模块变体数量(12:46)。
减少模块变体
Session 中演示了 ResearchKit 项目的优化过程。某个 target 有额外的 ENABLE_FEATURE 宏,导致 UIKit 等模块需要额外变体。把这个宏从 target 级别移到 project 级别后,模块变体数从 4 个降到 3 个(13:14)。
原则:尽量把 build settings 设置在尽可能高的层级(project 或 workspace),而不是 target 级别。这样模块可以在源文件之间最大化共享(14:28)。
核心启发
-
做什么:在混合 C/Objective-C/Swift 项目中启用 Explicitly Built Modules。为什么值得做:这是 Xcode 16 对 C/ObjC 的默认行为,构建更可靠、更高效,调试器不再重复编译模块。怎么开始:Build Settings 中搜索 “Explicitly Built Modules”,Swift 目前是 preview 状态,可以先在小项目上试验。
-
做什么:用 Build with Timing Summary 和 modules report 诊断模块变体。为什么值得做:多余的变体意味着重复编译,浪费时间和 CPU。显式模块构建把这个问题从暗处暴露出来,你才能解决它。怎么开始:Clean Build Folder 后执行 Product > Perform Action > Build with Timing Summary,在构建日志中搜索 “modules report” 查看变体数量。
-
做什么:统一项目中的 build settings 以减少模块变体。为什么值得做:不同 target 设置不同的宏或语言版本会导致同一个模块被编译多次,拖慢构建。怎么开始:检查各 target 的预处理器宏、语言版本、ARC 设置,把可以统一的移到 project 级别。用 Build Settings 的 Levels 视图对比 target 和 project 的差异。
关联 Session
- What’s new in Xcode 16 — Xcode 16 的整体改进概览,包含构建、调试和诊断的更新
- What’s new in Swift — Swift 语言更新,包含 Swift Build 的新能力介绍
- Migrate your app to Swift 6 — Swift 6 迁移实战,涉及模块级别的渐进式迁移策略
- Xcode essentials — Xcode 开发工作流核心技巧,与构建优化配合使用
评论
GitHub Issues · utterances