9-13.【编译与优化】针对多模块或多 target 项目,有哪些有效的编译时间优化策略?

32 阅读3分钟

在多模块或多 Target 的大型项目中,编译时间的增长通常不是线性的,而是呈指数级。这是因为模块间的依赖拓扑结构决定了编译器的并发效率。

以下是针对多模块项目最有效的几种编译优化策略:


1. 优化模块依赖拓扑(编译防火墙)

大型项目最忌讳“中心化”的底层模块。如果 BaseModule 被所有 Target 依赖,一旦修改 BaseModule 中的一个枚举,整个项目都会重新编译。

  • 依赖扁平化:减少嵌套依赖。尽量让功能模块(Feature Modules)并行编译,而不是互相等待。
  • 接口与实现分离:创建一个仅包含协议(Protocol)的 Interface 模块,具体的 Implementation 模块依赖它。调用方只依赖接口模块,这样修改实现代码时,调用方无需重绘。
  • 显式导入(Explicit Imports) :在 Swift 6 中开启 Strict Concurrency 检查时,尽量精简 import 列表,避免无意间引入庞大的子依赖树。

2. 巧用编译模式(Compilation Mode)

针对多 Target 项目,Build Settings 中的配置差之毫厘,时耗谬以千里:

  • Debug 模式:使用 Incremental(增量编译) 确保所有模块的 Compilation Mode 在 Debug 下都是 Incremental。这样 Xcode 只会重新编译受影响的文件。
  • Release 模式:使用 Whole Module(全模块优化) 虽然 WMO 慢,但它能消除跨模块调用的性能损耗。
  • 禁用不必要的调试信息:在 Debug 模式下,设置 Debug Information FormatDWARF(不带 dSYM),这能显著减少 Linker 链接大量 .o 文件的时间。

3. 模块级缓存:利用二进制分发

对于相对稳定的底层组件,不要每次都从源码编译。

  • Swift Package Manager (SPM) 缓存:利用 Xcode 自动缓存机制。
  • xcframework 化:将那些几乎不改动的第三方库(如网络库、工具库)预编译为 .xcframework
  • 远程缓存工具:使用 BazelTuist 结合远程缓存。如果同事已经编译过某个模块且代码没变,你的机器直接下载二进制文件,跳过编译。

4. 优化 Swift 与 Objective-C 混编

在多 Target 混编项目中,桥接头文件(Bridging Header)是性能最大的“隐形杀手”。

  • 减少 Bridging Header 引用:每多一个引用,Swift 编译器就要解析一次庞大的 Obj-C 头文件索引。
  • 使用 @class@protocol:在 Obj-C 头文件中尽量使用向前声明(Forward Declaration),减少头文件嵌套带来的递归解析开销。
  • 开启模块化头文件:将 Obj-C Target 转换为 Framework,利用 modulemap 进行导入,这比传统的 #import 快得多。

5. 控制泛型特化与内联的边界

虽然 @inlinable 能提升运行速度,但在多模块项目中,它是编译时间的杀手。

  • 限制 @inlinable 的规模:如果一个函数被标记为 @inlinable,任何对它的修改都会导致调用该函数的所有模块(即使是其他 Target)强制重新编译。
  • 按需特化:仅在确定为性能热点的跨模块调用上使用 @inlinable

6. 工具链诊断与量化

如果不量化,优化就无从谈起。

  • Build Timeline:Xcode 14+ 自带了可视化编译时间轴。通过 Product > Perform Action > Build with Timing Summary 查看哪个 Target 阻塞了编译流水线。
  • XCLogParser:这是一个强大的开源工具,可以把 Xcode 编译日志转换成详细的 HTML 报告,精确到每个文件的编译、链接和索引耗时。

总结建议:分阶段优化

  1. 第一阶段:检查 Debug 配置,确保没有开启 WMO,且调试格式为 DWARF。
  2. 第二阶段:识别并重构“中心化”模块,解开循环依赖和超大模块。
  3. 第三阶段:引入二进制方案(如 Tuist 或缓存二进制库),将核心库从源码构建中剥离。