软件设计

[模块化 #5] Xcode 构建变慢的原因与解决方法(含测量工具)

改完一行代码后按下构建,却要等上几分钟;每个 iOS 开发者都经历过这种情况。

4 分钟阅读
[模块化 #5] Xcode 构建变慢的原因与解决方法(含测量工具) 封面图

改完一行代码后按下构建,却要等上几分钟;每个 iOS 开发者都经历过这种情况。

假设每天构建几十次,每次缩短1分钟,就能毫不夸张地每天多出30分钟以上。

先说核心答案:Xcode 构建变慢的最大结构性原因,是稍有改动就会触发过大范围的重新编译;模块化正是缩小这一范围的解决方案。

本文将按层次拆解构建变慢的原因,说明模块化在哪些地方有效,并介绍如何用数字衡量优化前后的差异。

下面是重点总结。

  1. 增量构建的重新编译范围会被限制在模块内。没有模块时,整个应用就是一个单元。
  2. 不要在没有测量的情况下调优。Xcode 的 Build With Timing Summary 是基础工具。
  3. 除了模块化,还要检查类型推断瓶颈、dSYM 设置等局部原因。

为什么 Xcode 构建会变慢?

原因可以分为三个层次。

第一,重新编译范围。

Swift 文件发生变化时,Swift 会重新编译其所属模块中受影响的文件。但未模块化的应用会把整个目标视为一个模块。

在拥有几十万行代码的单一目标中,即使是细小修改也会引发大范围重新编译。尤其是修改被多处使用的类型时,基本就接近一次完整构建。

第二,类型检查瓶颈。

Swift 编译器会花费大量时间进行类型推断。一个复杂表达式可能就要耗时数秒。长链式调用、复杂三元运算符和庞大的 SwiftUI body 都很常见。

第三,构建设置。

如果 Debug 构建生成了 dSYM(DWARF with dSYM File),或错误地启用了 Whole Module Optimization,每次构建都会增加不必要的成本。


模块化如何改善构建速度?

模块化改善的是第一层:重新编译范围。

模块边界是重新编译的防火墙。只要模块内部实现发生变化,模块外部就不需要重新编译。

上一篇文章中的分层结构在这里发挥了作用。

  • 修改搜索功能(FeatureSearch)的内部实现时,重新编译只需涉及 FeatureSearch 和应用目标。
  • 相反,如果修改所有人都依赖的下游模块(CoreNetwork)的公开接口,上层模块就会依次重新编译。

因此,可以直接得出面向构建速度的模块设计原则。

  1. 将经常变化的代码放在图的上游(Feature),将稳定代码放在下游(Core·Domain)。
  2. 尽量缩小下游模块的 public 接口(不是 public 就不会影响外部)。
  3. 不要创建所有人都依赖的臃肿 Common 模块。

并行处理也有帮助。没有依赖关系的模块会由 Xcode 同时编译,因此图越宽、越浅,完整构建也越快。

有了模块边界,重新编译会在此范围内结束
有了模块边界,重新编译会在此范围内结束

如何测量构建时间?

凭感觉进行优化通常会偏离目标。三个工具就够了。

Build With Timing Summary。 在 Xcode 中执行 Product → Perform Action → Build With Timing Summary,构建日志末尾会汇总各阶段耗时。先在这里确认哪个目标、哪个阶段是瓶颈。

类型检查警告标志。 编译器会直接指出耗时较长的函数和表达式。将其添加到 Other Swift Flags。

-Xfrontend -warn-long-function-bodies=100
-Xfrontend -warn-long-expression-type-checking=100
// 100ms 对超过限制的函数和表达式发出警告

构建时间线。 打开构建日志 Assistant 区域中的时间线,可以看到各目标的并行执行情况。如果某些区段长时间串行排列,说明依赖图阻碍了并行处理。


除了模块化,还要检查什么?

测量后经常会发现,问题不是模块化,而是某一行设置。以下是针对 Debug 构建的检查清单。

项目 推荐设置(Debug)
Debug Information Format DWARF(关闭 dSYM 生成)
Compilation Mode Incremental
Optimization Level -Onone
Build Active Architecture Only Yes

从 Xcode 16 开始,Explicitly Built Modules 改善了模块准备阶段的并行处理。使用最新 Xcode 本身也能提升构建速度。

面试时可以这样问

Q. 请说明在大型 iOS 应用中缩短构建时间的方法。

首先用 Build With Timing Summary 测量瓶颈,并检查设置(Debug 下的 dSYM 生成和编译模式)。在结构上,关键是通过模块化将重新编译范围限制在模块内,把经常变化的代码放在依赖图上游,并最小化下游模块的 public 表面积。

Q. 拆分模块后构建仍然很慢,你会怀疑什么?

检查所有人依赖的下游模块是否经常变化,以及 public 接口是否频繁修改。再用警告标志找出类型检查瓶颈、拆分复杂表达式,并通过构建时间线确认依赖图是否被串行化,从而阻碍并行编译。

每天累计下来,等待构建的时间比想象中更长
每天累计下来,等待构建的时间比想象中更长

构建速度是模块化最直观的回报,但模块增加到几十个后,项目管理本身又会变成一项工作。

本系列最后一篇将介绍降低这项管理成本的工具 Tuist,也会整理摆脱 pbxproj 冲突和使用二进制缓存的方法。