Demystify explicitly built modules
System Frameworks 进阶 20m

深入理解显式构建模块

Demystify explicitly built modules

2024年6月10日

在 Apple 官方观看视频

一句话判断

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 构建和调试器维护着两套完全独立的模块图。当你在调试器中使用 ppo 计算表达式时,调试器需要知道 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 依赖的同一个模块只会编译一次。
WWDC 2024