在 GitHub Actions + Xcode 的典型 iOS 工程中,iOS CI 构建时间慢常被误认为是正常现象。真实原因是:iOS CI slow ≠ Xcode slow,而是 pipeline 无状态 + 无缓存 + 串行设计。
本文基于 8 人 iOS 团队真实案例(46 万行 Swift、28 Pods、14 SPM、日均 30+ push),将 GitHub Actions macOS CI 从 28 分钟优化到 9 分钟,附完整 iOS CI optimization 方案。
一、问题定义:为什么 iOS CI 会慢?
多数团队默认接受 20–30 分钟延迟。根因不是 Xcode,而是 CI pipeline 结构设计。
1.1 GitHub Actions CI 的本质问题
GitHub Actions macOS runner 是 ephemeral environment(临时环境)。每次执行都会导致:
- ❌ DerivedData 丢失(无法 incremental build)
- ❌ CocoaPods 每次重新 install
- ❌ SPM 每次重新 resolve
- ❌ build context 无法复用
结果:每次都是 cold build。
1.2 基线构建时间拆解(28 分钟)
用 workflow_dispatch 冷跑 3 次取中位数,拆分各阶段耗时:
| 阶段 | 耗时 | 类型 |
|---|---|---|
| Checkout | 45s | 固定 |
| pod install | 3m 12s | 网络依赖 |
| SPM resolve | 1m 44s | 解析 |
| xcodebuild build | 11m 08s | cold compile |
| xcodebuild test | 6m 22s | CPU |
| xcodebuild archive | 5m 30s | 串行阻塞 |
| upload/sign | 1m 05s | 固定 |
| Total | 29m 46s |
核心瓶颈:iOS CI 慢的三大原因
- Cold Build(39%)——无 DerivedData,每次全量编译 46 万行 Swift
- Dependency Re-resolve(17%)——Pods + SPM 每次重新下载/解析
- Serial Pipeline(19%)——test 与 archive 无依赖却串行执行
二、优化目标与结果
优化结果(iOS CI optimization 总览)
| 阶段 | 构建时间 |
|---|---|
| baseline | 28 min |
| + cache | 20 min |
| + parallel | 12 min |
| + Apple Silicon | 9 min |
优化收益拆解
- Cache:42%(约 8 分钟)
- Parallelization:32%(约 6 分钟)
- Apple Silicon:26%(约 3 分钟)
案例背景(E-E-A-T)
| 参数 | 值 |
|---|---|
| Swift 代码量 | 约 46 万行(含测试) |
| CocoaPods / SPM | 28 Pods + 14 packages |
| 基准 Runner | GitHub macos-latest(Intel,4 核) |
| 团队 / 频率 | 8 人,日均 30+ push |
| 测量方法 | 每步加时间戳,冷跑 3 次取中位数 |
单变量实验(各配置独立跑 10 次取中位数):仅加缓存 -29%、仅拆并行 -21%、仅换 Apple Silicon -18%。结构优化(缓存 + 并行)贡献 74%,硬件是加速上限而非第一因素。 p95 从 33 min 降至 12 min,DerivedData 命中率 80%。
三、优化方案一:CI 缓存(最大收益)
3.1 为什么缓存是关键
iOS CI optimization 的本质是:让 Xcode incremental build 在 CI 中重新生效。 必须把 DerivedData、CocoaPods cache、Swift Package Manager cache 三个目录持久化。
3.2 缓存三要素
- DerivedData:
~/Library/Developer/Xcode/DerivedData - CocoaPods cache:
~/Library/Caches/CocoaPods - SPM cache:
~/.spm-cache
3.3 GitHub Actions 配置
- name: Cache DerivedData
uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: deriveddata-${{ runner.os }}-${{ hashFiles('**/Podfile.lock','**/Package.resolved') }}
- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: ~/Library/Caches/CocoaPods
key: pods-${{ runner.os }}-${{ hashFiles('**/Podfile.lock') }}
- name: Cache SPM
uses: actions/cache@v4
with:
path: ~/.spm-cache
key: spm-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }}
3.4 缓存 key 设计要点
缓存命中率决定 DerivedData cache 能否真正生效。key 设计有三个常见层级:
| key 策略 | 命中率 | 问题 |
|---|---|---|
只用 runner.os | ~100% | 可能命中旧 Pods,产物不一致 |
| 锁文件哈希(推荐) | ~80% | 依赖不变就命中,更新才重建 |
| 包含 commit SHA | ~0% | 每次 push 都 miss,等于没缓存 |
推荐用 Podfile.lock / Package.resolved 哈希作为主 key,并配置 restore-keys 前缀匹配:主 key miss 时仍能拿到上一次有效缓存,触发增量编译而非完全 cold build。CI 里务必用 pod install --no-repo-update,避免每次改写 lock 文件导致缓存失效。
详见 GitHub Actions cache 文档;远程缓存方案对比参阅 DerivedData / Pods / sccache 对比。
3.5 缓存效果
| 项目 | 优化前 | 优化后 |
|---|---|---|
| pod install | 3m 12s | 50s |
| SPM resolve | 1m 44s | 15s |
| build | 11m | 3–4m |
平均节省 8–10 分钟。
四、优化方案二:CI 并行化(Pipeline optimization)
4.1 问题
默认 CI:build → test → archive(串行)。但 test 和 archive 没有依赖关系,可以并行执行。
4.2 并行 Job 设计
jobs:
build-and-test:
runs-on: macos
steps:
- run: xcodebuild build test
archive:
runs-on: macos
if: github.ref == 'refs/heads/main'
steps:
- run: xcodebuild archive
archive 只在 main 触发;各 job 独立恢复缓存。证书用 Fastlane Match 管理。资源隔离参考 3 台 Cloud Mac 撑住每天 500 次 iOS CI。
4.3 效果
| 模式 | 时间 |
|---|---|
| serial | 20 min |
| parallel | 12 min |
节省 5–7 分钟。
五、优化方案三:Apple Silicon Runner
5.1 为什么要放最后
硬件不是解决 iOS CI slow 的第一因素。 无缓存时换 Apple Silicon 仅省 18%;结构优化完成后再升级,才能从 12 min 压到 9 min。
5.2 实测提升(缓存 + 并行已开启)
| 阶段 | Intel | Apple Silicon |
|---|---|---|
| build | 3m 40s | 1m 55s |
| test | 6m 22s | 3m 48s |
| archive | 5m 30s | 3m 10s |
5.3 结论
额外节省 3–5 分钟。 Apple Silicon 在 Swift 编译和链接阶段提速最明显,参见 Apple 增量构建文档。
5.4 三种 macOS Runner 方案对比
结构优化完成后,团队通常在三类 GitHub Actions self-hosted runner macOS 方案中选择:
| 方案 | 构建时间 | 队列 | 维护成本 |
|---|---|---|---|
| GitHub macos-latest | ~20 min | 高(峰值等 10–20 min) | 零 |
| 自建 Mac mini(Intel) | ~14 min | 无 | 高 |
| Cloud Mac(Apple Silicon) | ~9 min | 无 | 低 |
GitHub 托管 runner 免费但有队列瓶颈,且缓存存储受 10 GB/repo 限制,大型工程容易频繁 evict。自建消灭队列但 Intel 硬件有天花板。对于日均 30+ push 的团队,独享 Apple Silicon 节点通常 ROI 更高——结构优化自己做,硬件和队列问题再单独评估。
六、iOS CI optimization 决策树
CI > 15 min?
↓
cold build > 40%?
→ YES: enable cache (DerivedData + Pods + SPM)
↓
pipeline serial?
→ YES: split jobs (test / archive)
↓
still slow?
→ upgrade Apple Silicon runner
可复现 Runbook(5 步):
- 建立基线:每 step 加
echo "::notice::$(date -u +%H:%M:%S)",冷跑 3 次取中位数 - 加缓存:DerivedData + Pods + SPM,连跑 10 次统计命中率(目标 > 60%)
- 拆 workflow:test / archive 独立 job,archive 仅 main 触发
- 评估硬件:结构优化后仍 > 12 min 再考虑 Apple Silicon runner
- 监控 p95:目标 ≤ 1.5× 均值,超基线 20% 告警
七、常见误区(iOS CI Optimization Mistakes)
❌ Xcode incremental build in CI works automatically
错误。CI 没有 DerivedData,必须显式缓存。
❌ upgrade Mac solves CI slow
错误。未缓存情况下提升有限(约 18%)。
❌ increase -jobs always faster
超过 CPU core 会变慢。M2 推荐 -jobs 6~8。
❌ cache key should be exact
commit SHA 会导致 cache miss。用 Podfile.lock / Package.resolved hash。
八、FAQ(GitHub Actions iOS CI 高频问题)
Q1:为什么 GitHub Actions iOS CI 很慢?
因为 GitHub Actions macOS runner 是 stateless ephemeral environment,不保留 DerivedData、Pods 或 SPM 缓存。本机有热缓存,CI 每次从零 cold build,即使本地 Xcode build time 很快。
Q2:iOS CI optimization 最有效方法是什么?
缓存 DerivedData + CocoaPods + SPM,贡献 42%,平均节省 8–10 分钟。这是 Xcode build time optimization 在 CI 环境里的第一优先级。
Q3:Apple Silicon 能解决 CI slow 吗?
不能单独解决,只是加速上限。无缓存时仅省 18%;配合缓存和并行化后,才能将构建时间从 12 min 压到 9 min。
Q4:Pods 和 SPM 哪个更慢?
CocoaPods 通常更慢(3m 12s vs 1m 44s),因为涉及网络下载。两者都应缓存,收益叠加。
Q5:最佳优化顺序?
Cache → Parallelization → Apple Silicon。三步组合压缩 68%,顺序不可跳步。
九、结论
iOS CI 慢的本质不是性能问题,而是:
补充:完成结构优化后,若需独享 Apple Silicon self-hosted runner 且无硬件运维,可参考 Cloud Mac 方案(队列零等待、缓存不受 10 GB/repo 限制)。