以前对编译慢的态度一直是忍着——"反正也就十几秒",直到接手一个模块多的大工程:改一行 Model,等两分钟,回来看还在 Link。忍了几天之后开始认真折腾,发现提速这件事没那么玄,前提是别瞎调参数,先找到 Xcode 把时间花在哪。Build Settings 里搜 "timing",打开 Show Build Timing Summary(命令行就是下面这样),构建完每个文件的编译耗时、每个阶段占多少,全都列出来:
xcodebuild -showBuildTimingSummary build
# 或在 Xcode 里:Build Settings 搜索 timing → Show Build Timing Summary 打勾
量完再动手,命中率高很多——这份报告里的大头,基本逃不出下面这几层,挨个说。
第一站:读懂耗时报告
报告的读法很简单:Compile Sources 段最长,问题在代码和编译模型;某个 Run Script 阶段横着占一条,基本就是脚本阶段出了问题(下面的警告就是讲这个);Link 段偏长,看依赖的数量和形态。
细节还可以再往下钻一层:Compile Sources 的明细里,每个文件编了多久是逐个列出来的,挑最慢的那几个看一眼,十有八九就是类型检查或语法复杂度的重灾区。有了这个"哪层最贵"的判断,后面的排查就不是碰运气了。
工程配置层:三个容易被忽略的设置
Debug 配置的优化等级。 Debug 构建应该用 -Onone(不做优化)+ 增量编译:单文件改动只重编受影响的文件。如果这里被改成 -O 或者开了 Whole Module Optimization,那"改一行、整模块重编"就是必然的——检查 Swift Compiler - Code Generation 里的 Optimization Level,Debug 栏是不是 None。这条被改错的概率不低(有人为了"跑得快一点"动过它)。
Run Script 阶段有没有声明输入输出。 这是大工程"每次都全量重编"的经典元凶:脚本阶段如果没在 Input Files / Output Files 里声明依赖关系,Xcode 无法判断它会不会产生新改动,于是每一次构建都重跑它;如果这个脚本还改了什么文件,就会连锁触发大规模重编。修起来很简单——把脚本读写哪些文件声明清楚,构建系统才能跳过没必要的执行。
一个侧面验证:如果你的工程"什么都没改也编译半天",先去查这两条:Run Script 的输入输出、Debug 的优化等级。这俩能解释大半的"莫名重编"。
只编译当前架构。 Debug 下 Only Active Architecture 保持 Yes,避免 arm64 和 x86_64 两份都编——模拟器调试时白等一倍时间,纯属没必要嘛。并行编译本身默认是开的(按 CPU 核数分配编译任务),但有个反向的坑:内存不够的机器上,并行度太高会频繁换页,反而更慢——老机器上可以把并行任务数调低试试,有时候"稳定地慢"比"忽快忽慢"还好排查。
代码结构层:控制"重编的爆炸半径"
改一个文件连累多少重编,取决于工程的模块划分。理想状态下,稳定的基础层(工具函数、模型)放在独立的 Swift Package 或 framework 里,改上层的业务代码时,底层不需要重编。反过来,如果所有代码塞在一个大 target 里,改任何一个文件都可能触发一大片的重新编译——模块化的第一收益不是"架构整洁",就是编译速度,这点挺实在。
还有一个容易被当成玄学的点:类型检查也会吃时间。一个表达式里链了五六个 map/flatMap、或者巨型字面量嵌套,类型推断能算上一会儿(写过那种"编译不动、拆开就好了"的代码的都懂)。把复杂表达式拆成几行、给关键的地方标上显式类型,编译器轻松,你也不用对着"Expression too complex"发呆。
Objective-C 和 Swift 混编的项目注意桥接头:bridging-header 里暴露的头文件越多,改一个公共头触发的重编面越大。不是不能混编,是这个账要心里有数。如果经常出现"改 A 却重编了 B、C、D"的情况,翻构建日志找原因吧——每个编译任务的触发链路是有记录的,对着日志看,能把意外的依赖揪出来。
依赖与缓存层:别让它重复劳动
依赖(SPM、CocoaPods)的构建产物有没有被缓存住,直接影响每次构建的起跑线。这里有个反直觉的点要专门说:DerivedData 别乱清。网上很多"Mac 清理指南"把它当垃圾目录,删了确实立刻腾出几十个 G——代价是下一次构建全量重来。清理之前先想一下:你是缺磁盘,还是缺时间呢?两个都缺的话,它不该是第一个动的。
CI 上的思路同理:把依赖缓存和构建缓存做进流水线,别每次从零开始。
判断增量编译到底有没有在工作,有个土办法:改一行代码再构建一次,数一数日志里实际编译的文件数量——只编了一个,说明增量正常;编了一串,就回去查依赖和脚本阶段,问题八成在那边。这个"数文件"的办法比任何感觉都准。
工具链与开发循环层
到这一层,跳出来看整个"改代码到看到效果"的通路。Xcode 里跑真机是 Run,出包是 Archive → 导出 IPA → 安装,几段路各有各的沉默时间。用 KXApp 这类轻量环境的做法是把调试循环压短——连上 iPhone 一键构建并安装到真机,不经过"导出 IPA 再想办法装"那一段绕路;改完代码快速同步到手机,看效果的间隔短了,感知上"编译慢"的抱怨也会少一截。
下次觉得慢就可以按照这个顺序来尝试
先看 Timing Summary 找大头 → 查 Run Script 的输入输出声明 → 确认 Debug 没开优化和 WMO → 再看要不要拆模块、理依赖。别一上来就清 DerivedData——那只会让下一次更慢,属于帮倒忙。
编译速度是那种平时不觉得、卡起来真烦的东西。花一个下午把上面几层过一遍,换来的是往后每一天都少等的那几分钟——这笔账怎么算都划算啊。要是连工具链本身都想换个轻点的形态,KXApp 这类免 Xcode 的环境把"改—跑—看"收在一处,配合上面这些工程层的优化,日常的循环会舒服不少。