深入理解显式构建模块
Demystify explicitly built modules
2024年6月10日
一句话判断
Xcode 16 把模块构建从编译器的隐式行为变成了构建系统的显式任务,你的项目编译会因此更可靠、更快——特别是那些混编 Swift 和 Objective-C 的大型项目。
这场 Session 讲了什么
这场 Session 解释了 Xcode 16 引入的 explicitly built modules(显式构建模块)机制。在此之前,模块构建是编译器的隐式行为——每个编译任务自己发现需要的模块,如果没有编译好的就自己编译,多个编译任务之间通过隐式协调避免重复工作。这导致了构建时间不可预测、错误难以复现、调试器需要重新构建模块等问题。
Xcode 16 把这个流程拆成三个显式阶段:scan(扫描源文件构建模块图)、build modules(编译模块)、compile sources(编译源文件)。构建系统完全了解模块依赖关系,可以做出更好的调度决策。对于 C 和 Objective-C 代码,这是默认启用的;对于 Swift,可以通过 Build Settings 开启(preview 阶段)。
值得深挖的点
隐式 vs 显式模块构建的根本区别
隐式模式下,编译器之间通过文件系统的存在性来协调——一个编译任务在需要 UIKit 的 .pcm 文件时,如果发现文件不存在就开始编译它,其他需要 UIKit 的编译任务要么等待要么自己也尝试编译。这种”谁先到谁编译”的模式在大型项目中导致严重的调度问题:某些编译任务会在开始时阻塞很久(等前面的任务编译模块),而构建系统完全不知道发生了什么,以为这些任务就是慢。
显式模式把模块构建变成了构建系统的一等公民。构建系统先扫描所有源文件,构建完整的模块依赖图,然后按正确的拓扑顺序调度模块编译任务,最后才编译源文件。这意味着:不会有编译任务被隐式阻塞,构建系统可以充分利用所有可用的执行通道,clean build 的行为完全确定可复现。
调试器的模块复用
隐式模式下,Xcode 构建和调试器维护着两套完全独立的模块图。当你在调试器中使用 p 或 po 计算表达式时,调试器需要知道 Swift 类型信息,所以它需要自己构建一套模块——这意味着你调试时又要等一轮模块编译。
显式模式下,调试器直接复用构建过程中已经编译好的模块。这消除了调试会话开始时的延迟,也让调试器看到的类型信息和构建时完全一致。对于大型 Swift 项目,这个改进对日常开发效率的影响可能比编译速度提升更明显。
代码片段
启用 Swift 的显式模块构建
场景:在 Xcode 16 中为 Swift 启用 explicitly built modules。
// 步骤:
// 1. 在 Project Navigator 中选择项目
// 2. 进入 Build Settings
// 3. 搜索 "explicitly built"
// 4. 将 Explicitly Built Modules 设置为 Yes
// 注意:C 和 Objective-C 默认已启用
// Swift 目前是 preview 状态,需要手动开启
坑:Swift 的显式模块构建还在 preview 阶段,某些复杂项目可能会遇到问题。建议先在非关键项目上验证。
在构建日志中识别显式模块任务
场景:理解新的构建日志结构。
// 构建日志中会出现三类新任务:
// 1. Scan 任务 - 扫描源文件的模块依赖
// Scan [target] [source_file]
// 这是内置任务,不会启动新进程
// 2. Module compile 任务 - 编译模块
// CompileSwiftModule [module_name]
// 或 CompileC [module_name].pcm
// 3. 原始编译任务 - 现在会接收已编译模块的路径
// CompileSwiftSources [source_file]
// (自动注入依赖的模块路径)
坑:构建日志会变得更长(每个源文件都有 scan 任务),但总编译时间应该更短。不要被日志行数吓到。
最大化模块复用
场景:通过模块复用来优化构建速度。
// 确保跨 target 的共享模块能被复用:
// 1. 使用相同的 build settings
// - 相同的 Swift 语言版本
// - 相同的 optimization level
// - 相同的其他编译标志
// 2. 注意 module map 的一致性
// 如果两个 target 以不同方式定义同一个模块,
// 构建系统会认为它们是不同的模块,无法复用
// 3. Explicitly built modules 会在 target 之间
// 自动共享模块(如果 settings 兼容)
坑:不同 target 使用不同的编译标志会导致相同源码编译出不同的模块,浪费构建时间。
最佳实践
已有项目:对于包含 C/Objective-C 代码的项目,explicitly built modules 已经默认启用,你应该能在构建日志中看到新的 scan 和 module compile 任务。对于 Swift 代码,等它从 preview 变成正式版后再全面启用,现在可以先在小项目上试验。关键是检查构建日志,确认模块复用正常工作。
新项目:在 Xcode 16 中创建的新项目已经对 C/ObjC 使用显式模块构建。如果项目包含 Swift,考虑在 Build Settings 中手动开启。保持 build settings 的一致性(特别是跨 target)能最大化模块复用。如果项目中混合使用 Swift 和 Objective-C,显式模块构建的收益最大。
还有什么值得关注
- Explicitly built modules 让 clean build 也能正确重建所有模块,不像隐式模式可能残留旧状态。
- 构建日志中的 scan 任务是内置的(不启动新进程),所以不会增加进程开销。
- 模块的编译结果可以跨 target 共享——只要 build settings 一致,两个 target 依赖的同一个模块只会编译一次。