发布前两天临时增加一台远程 Mac,代码拉取只用了几分钟,第一次 Xcode 构建却持续等待;第二次虽然变快,切换分支后又出现旧资源、索引异常或依赖重新解析。问题通常不在机器是否够快,而在 DerivedData、Swift 包下载和构建结果混在默认目录中,既无法判断缓存是否命中,也无法在租期结束前有选择地迁出。
先定义可比较的构建基线
缓存调优前先固定变量。记录 xcodebuild -version、macOS 版本、提交哈希、Scheme、构建配置和目标平台。测试期间不要同时升级依赖、切换 Xcode 或修改编译参数,否则两次耗时没有可比性。
至少执行三组测试:
- 删除项目专属 DerivedData 后进行冷构建。
- 不改代码,使用相同命令进行暖构建。
- 修改一个普通 Swift 文件,再进行增量构建。
每次保存开始时间、结束时间、退出码和结果包。图形界面的进度条适合观察,不适合精确比较;自动化记录应统一走 xcodebuild。
缓存的目标不是让某一次构建看起来最快,而是让相同输入得到可预测的耗时和结果。
把缓存按职责拆开
建议在工作目录下建立四类路径:源码、DerivedData、Swift 包下载和结果日志。不要让多个项目共用同一个 DerivedData,也不要让 Debug 与 Release 相互覆盖。项目较多时,可继续加入 Xcode 主版本和提交分支标识。
| 目录 | 保存内容 | 建议策略 |
|---|---|---|
Source |
Git 工作树 | 按项目独立 |
DerivedData |
索引与中间产物 | 按版本、Scheme、配置隔离 |
SourcePackages |
Swift 包下载与检出 | 锁文件未变化时复用 |
Results |
结果包与日志 | 按构建时间归档 |
Swift 包目录可以复用,但前提是依赖锁文件一致。若锁文件发生变化,先让解析流程正常完成,不要直接把旧检出目录复制到新项目上。DerivedData 对工具链更敏感,切换 Xcode 后应创建新目录,而不是覆盖原目录继续使用。
用固定命令执行构建
下面的脚本把关键路径显式化。将工作区、Scheme 和目标平台替换为项目实际值。结果包路径在执行前必须不存在,因此脚本会先删除同名目录。
#!/bin/bash
set -euo pipefail
ROOT="${HOME}/BuildWorkspace"
SCHEME="Application"
CONFIGURATION="Release"
DERIVED="${ROOT}/DerivedData/${SCHEME}-${CONFIGURATION}"
PACKAGES="${ROOT}/SourcePackages"
RESULT="${ROOT}/Results/${SCHEME}.xcresult"
LOG="${ROOT}/Results/${SCHEME}.log"
mkdir -p "${DERIVED}" "${PACKAGES}" "${ROOT}/Results"
rm -rf "${RESULT}"
xcodebuild \
-workspace "${ROOT}/Source/Application.xcworkspace" \
-scheme "${SCHEME}" \
-configuration "${CONFIGURATION}" \
-destination "generic/platform=iOS" \
-derivedDataPath "${DERIVED}" \
-clonedSourcePackagesDirPath "${PACKAGES}" \
-resultBundlePath "${RESULT}" \
build | tee "${LOG}"
先在终端运行 xcodebuild -list,确认工作区确实暴露了目标 Scheme。若项目使用工程文件而非工作区,把 -workspace 改为 -project。不要把两者同时传入。
为缓存目录生成稳定标识
缓存键至少应包含 Xcode 版本、依赖锁文件摘要、Scheme 和配置。分支名可以加入,但不要只用分支名,因为同一分支的依赖可能已经变化。
XCODE_KEY="$(xcodebuild -version | shasum -a 256 | cut -c1-12)"
PACKAGE_KEY="$(shasum -a 256 Package.resolved | cut -c1-12)"
printf '%s-%s-%s-%s
' "${XCODE_KEY}" "${PACKAGE_KEY}" "${SCHEME}" "${CONFIGURATION}"
工作区里若有多个 Package.resolved,应明确选择真正参与构建的文件。找不到锁文件时不要生成一个固定空键,否则不同依赖状态会落入同一目录。
按阶段定位无效重编译
暖构建仍然很慢时,先看耗时落在哪一段。依赖解析反复发生,检查锁文件是否被脚本重写、包目录权限是否正确;编译阶段反复执行,检查编译条件、自动生成文件和构建阶段脚本;链接阶段异常,检查是否每次都改变版本信息或嵌入内容。
最常见的问题是脚本阶段没有声明输入与输出。只要脚本每次改写生成文件的时间戳,下游目标就会被判定为需要重建。修复方法是让脚本仅在内容变化时替换文件,并在 Xcode 构建阶段中填写准确的输入、输出路径。
不要把“清空所有缓存”当作固定步骤。它只能回答缓存是否污染,不能解释污染来自哪里。先单独移动项目的 DerivedData;问题仍在,再处理 Swift 包目录。这样能保留诊断证据,也避免每次重新下载全部依赖。
租期结束前保留真正有用的内容
迁出前先停止正在运行的构建,确认日志已经写完,再打包依赖锁文件、必要的 Swift 包下载、结果包和项目脚本。源码应以代码仓库中的已提交内容为准,工作树里的未提交修改要单独检查。
不建议长期保存整个 DerivedData。它通常体积大,包含索引、对象文件和与具体工具链绑定的中间产物。下一次使用不同版本的 Xcode 时,重新生成往往更可靠。敏感证书、私钥、令牌和临时环境文件不要混入缓存归档;先清理 Shell 历史、临时钥匙串及项目生成的凭据文件,再核对导出包清单。
最后保留一份简短记录:工具链版本、缓存键、最后一次成功提交、构建命令、已导出路径和明确未导出的路径。下一台云端 Mac 按这份记录恢复,比复制整个主目录更容易复现。
常见问题
DerivedData 是否应该在多个 Xcode 版本之间共用?
不应该。应至少按 Xcode 版本、项目、Scheme 和构建配置隔离目录;切换工具链后重新生成 DerivedData,避免旧索引或中间产物造成隐蔽错误。
租期结束前最值得迁出的缓存是什么?
优先迁出依赖锁文件、可复用的 Swift 包下载目录、构建结果包和关键日志。对象文件与索引通常体积大且强依赖工具链,下一台机器重新生成更可靠。
清空全部缓存能否解决构建变慢?
只能用于确认缓存污染,不能作为日常方案。先记录解析依赖、编译、链接和测试各阶段耗时,再定点清理对应目录,才能避免每次都付出完整冷构建成本。
ArmMacs Cloud Mac
按项目周期使用独享物理节点
选择固定芯片、内存、存储、租期与在售节点,库存可用时进入交付流程。