WWDC Quick Look 💓 By SwiftGGTeam
Demystify explicitly built modules

Demystify explicitly built modules

观看原视频

Highlight

Xcode 16 把模块构建从编译器的隐式行为拆成三个显式阶段——scan、build modules、compile sources——让构建系统完全掌握模块依赖图,从而更高效地调度任务、避免执行通道被阻塞,并让调试器直接复用已编译的 Swift 模块。


核心内容

你有没有遇到过这种情况:同一个项目,clean build 和增量构建的时间差异巨大,而且增量构建的时间几乎不可预测?问题出在模块构建一直是编译器的隐式行为。每个编译任务遇到 import 时,自己去找模块、判断是否需要编译、如果别的任务正在编译就等着——整个协调过程对构建系统不可见。结果是执行通道被阻塞,你看到的编译任务时长参差不齐,开头的一批特别长,因为它们在等模块编译完成。更麻烦的是,调试器(LLDB)有自己独立的模块图,用 ppo 求值表达式时还得重新编译模块(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 把每个源文件的编译拆成三个阶段:

  1. Scan:扫描每个源文件,构建整个项目的模块依赖图,结果在构建日志中表现为 scan 任务。这是内置任务,不启动新进程,可以在源文件之间缓存信息(09:27
  2. Build modules:根据模块图调度模块编译任务,每个模块启动独立的编译器进程。这些任务是顶层的,不附属于任何特定 target,因为模块可以跨 target 共享(09:47
  3. 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

评论

GitHub Issues · utterances