Vuncloud 博客
← 返回机房手记专栏

iOS CI 构建很慢怎么办?GitHub Actions 上 Xcode build 变慢原因解析

机房手记 · 同一条 PR 有时 6 分钟、有时 18 分钟?先把「快与慢」拆开看 · cold / warm · 14 天 Shadow 实测 ·约 9 分钟阅读

Mac 屏幕显示 Xcode 与终端,排查 GitHub Actions iOS CI xcodebuild 构建变慢
TL;DR · 先看清,再动手
  • 187 次 PR 里,八成多是 warm——日常该盯的是 warm,别把 cold 混进 P95
  • 改依赖、清缓存、换 scheme 会「冷启动」,墙钟差 2–3 倍 很正常
  • 就算都是 warm,cache 没配好、多 job 抢磁盘、测试和签名搅在一起,仍会忽快忽慢
  • 大致顺序:排队 → 分清 cold/warm → 缓存 → 控并发 → 最后才谈换机器(对照见 瀑布拆解

完整对照数据 → GitHub Actions 优化主轴 · Benchmark

86.6%
Shadow 样本为 warm build
14:12→6:05
warm P95(macos-latest → 独享 M4)
2–3×
cold vs warm 典型墙钟差距

1. CI 像抽奖:同 commit,差几倍

在 GitHub Actions 上跑 iOS CI,下面这些画面应该不陌生:

  • 同一个 commit 重跑一遍,墙钟能差 2–3 倍
  • 看板 P95 红得吓人,大家体感却是「平时 merge 也没等那么久」
  • pod install 有时半分钟,有时像卡住了
  • 周五下午不敢点 merge——怕 CI 又「抽中」慢的那档

未必是机器算力不够。我们跟团队做过 14 天 Shadow 双轨,更常见的真相是:数字算混了,环境也不稳——cache 没留住、磁盘被抢,体感就像抽奖。换芯片往往排在很后面。

这篇只聊一件事:构建为什么忽快忽慢。job 卡在队列里等 runner?看 排队怎么破。缓存怎么配、买机租机划不划算,留给 cache 专题ROI 那篇

2. cold 和 warm:别混在一锅粥里

有个坑特别常见:把每次构建的耗时全倒进一个 P95。偶尔来一趟「冷启动」,尾巴就被拉很长——看数据觉得「天天都很慢」,其实日常 merge 没那回事。

怎么分 · Shadow 对照用的口径
  • warm:依赖没动、缓存还在、scheme 没换——多半是增量编译,代表「日常 merge 长什么样」
  • cold:锁文件变了、缓存清了、换 target 或 runner 上第一次跑——得重新 resolve、装 Pod、大面积重编

日常 SLA 盯 warm 的 P50/P95 就够了;cold 另画一条线,别和 merge 体验绑在一起。

2.1 什么情况会「冷启动」

macos-latest 上 cold 更常见——job 跑完 workspace 往往就清掉了,缓存很难「住下来」:

触发条件 典型耗时增量 日志里常见关键字
Podfile.lock 变更 +3–8 分钟 pod install、Downloading dependencies
DerivedData 未命中 / 被清 +5–15 分钟 CompileSwift、全量 .o 重建
切换 scheme / 新 target +2–10 分钟 不同 xcodebuild -scheme
SPM 依赖解析变更 +1–5 分钟 Resolve Package Graph
Xcode 小版本被动升级(macos-latest) 首次构建 +10–20 分钟 新 SDK / 模块缓存重建

依赖升得勤,cold 自然多——不是机器退化,是这周干的活不一样。汇报时把「升 Pod 那周」和「平常开发周」分开,数字才说得通。

2.2 warm 为啥还是不稳

就算都是 warm,墙钟仍可能上下飘个三成左右,常见是这些:

  • cache 没命中:key 少写了 arm64、没绑分支,或多个 job 抢同一个槽
  • 同一 org 里 macOS job 扎堆,磁盘和网络互相拖
  • 这次只编 main,下次全量 unit + UI test——工作量本就不一样
  • 改动面不同:动 Pod 源码和改个 SwiftUI 预览,compile 量差很远
开发者查看 GitHub Actions iOS CI 构建日志,分析 xcodebuild warm 与 cold 耗时差异

3. 时间到底耗在哪儿

给 workflow 各步打个时间戳,墙钟大致能拆成五块。下面是 warm 时比较常见的分布(项目不同会有出入):

iOS CI 墙钟五段(warm · 示意)
① checkout + 环境准备     ~0:30 – 1:30
② pod install / SPM resolve ~0:30 – 2:00   (cold 时 ↑↑)
③ xcodebuild compile+link   ~3:00 – 8:00   (代码变更面决定)
④ 测试(simulator / unit)  ~1:00 – 6:00   (可选,常被低估)
⑤ archive + codesign        ~1:00 – 4:00   (发版流水线)

合计 warm P50 常见区间:6 – 14 分钟

实用做法:用 step 耗时或 time 分别看 pod installxcodebuild buildxcodebuild test。② 经常五分钟往上——先想 cold 和 cache;③ 忽长忽短——查 DerivedData 和并发;④ 总是拖后腿——测试拆出去,或放 nightly 全量。

4. 测试和签名:隐形拖后腿

不少「xcodebuild 好慢」的反馈,拆开看是测试或签名被算进同一次 build

  • Simulator 冷启动:CI 上第一次拉起可能要等多几分钟,没预热就每 job 重来一遍
  • UI Test 比 unit 慢一个量级,和 compile 塞一个 job 里,P95 很难看
  • 证书、Keychain、Profile 下载——托管 runner 上常常每次 job 重新配一遍
  • Archive、打 IPA 是发版流水线的事,别和 PR 验证的耗时搅在一起
小建议 · PR 和发版分开算

PR:build + 轻量测试,盯 warm P95。
TestFlight / 发版:单独 workflow、单独指标。混在一处,「平时 merge 要等多久」永远对不上号。

5. 14 天 Shadow 实测

下面是一组双轨对照:macos-latest 和独享 Mac mini M4 各跑 187 次 PR,Xcode 16.2、CocoaPods 1.15.2 对齐。完整做法见 主轴 Benchmark

分类 样本数 占比 macos-latest P95 独享 M4 P95
warm build 162 86.6% 14:12 6:05
cold build 25 13.4% 19:40 11:20
混在一起算(容易误判) 187 100% ~16:00+ ~7:30+

如果把 cold 和 warm 搅在一起报 P95,日常体验会被高估大概 15–25%,容易得出「得马上换机器」的结论。更稳的做法:merge 体验看 warm,升依赖那几周单独预期 cold

独享 M4 上 warm 仍比 cold 快一截,说明让 DerivedData、Pods 有固定「家」很值——详见 CocoaPods / SPM / DerivedData 缓存优化

6. 怎么排:从看清到 cache

排队问题排除(或已经不大)之后,可以按这个顺序来,别一上来就谈买机器:

  1. 给构建打标签:cold 还是 warm(Podfile.lock 变没变、cache 中没中)
  2. 日常 SLA 只盯 warm P95;cold 周报或单独一条线
  3. 把 DerivedData、Pods、SPM 的缓存策略定下来 — CocoaPods / SPM / DerivedData 缓存优化
  4. macOS job 别挤太满:self-hosted 一台 1–2 个并发就够;托管侧也尽量别多 job 共 workspace
  5. PR 验证和发版/signing 拆开,别让测试污染 merge 指标
  6. cache 都到位了还不达标,再聊 M4 / M4 Pro — 见 芯片与工时
workflow 片段 · 标记 warm / cold(示意)
- name: Classify build type
  run: |
    if git diff --name-only HEAD~1 | grep -q Podfile.lock; then
      echo "BUILD_TYPE=cold" >> $GITHUB_ENV
    else
      echo "BUILD_TYPE=warm" >> $GITHUB_ENV
    fi

- name: Record wall clock
  run: |
    echo "build_type=${BUILD_TYPE}" >> metrics.csv
    echo "wall_sec=$(( $(date +%s) - START ))" >> metrics.csv

7. 常见问题(FAQ)

同 commit 重跑差 2–3 倍,正常吗?

macos-latest 上挺常见:一回 cache 全中,一回 miss 叠上排队,就像两档不同的 build。先看两次 pod install 和 cache hit 日志,别急着换芯片。

P95 到底怎么算才对?

日常 merge 的体验,用 warm 算 P95 就行,两周攒够三十次样本比较稳。cold 另算或只记次数——混在一起,采购决策容易偏。

pod install 每次都很慢,是 CocoaPods 的锅吗?

托管 runner 上更常是 Pods 没地方「住」、cache key 没绑分支和架构。self-hosted 指到固定大盘,warm 时半分钟以内很常见。

换 Mac mini M4 能消除波动吗?

对照里 warm P95 能降一大截(−57%),波动也收敛(σ −40%),但分不清 cold/warm、cache 没设计好,独享机照样会有尖刺。

慢是排队还是 build 本身?

看日志开头 Waiting for a runner 等了多久。排队算墙钟、不算 GitHub 分钟费。queue 几乎为零还慢,就是这篇聊的构建侧 → 接着看 cache。

还想往下读?

缓存 YAML 和 key 怎么设计 → cache 专题;买还是租、划不划算 → ROI 模型月构建 500 次怎么选

8. 收个尾

GitHub Actions 上 iOS CI 「慢」,很少是单纯 CPU 不够。更常是这几件事叠在一起:

  1. cold 和 warm 混在一起看,P95 被抬高了
  2. 托管 job 跑完就清盘,缓存留不住
  3. 测试、签名和 PR build 搅在一个 job 里

先把 warm 的 P95 算清楚,再 cache → 并发 → 硬件。14 天 Shadow 里,只做对指标和缓存,warm P95 就能从 14:12 拉到 6:05——不必先拍板买机器。

想让 DerivedData 有个固定「家」?

Vuncloud Cloud Mac M4 Pro 带 1TB 数据盘、预装 actions-runner,DerivedData / Pods 可以一直留在盘上,托管 runner 那种「每次冷启动」会少很多。

查看 Cloud Mac 套餐 · CI/CD 接入 FAQ

机房手记 · iOS CI

先把快慢看清,再谈换芯片

cold / warm 分开算 · cache 优先 · Shadow 双轨一周见分晓

读主轴 Benchmark
限时优惠 点击查看套餐