在 GitHub Actions + Xcode 的典型 iOS 專案中,iOS CI 建置時間慢常被誤以為是常態。真正的癥結在於:iOS CI slow ≠ Xcode slow,而是 pipeline 無狀態、沒快取、又採直列設計。
本文以 8 人 iOS 團隊的真實案例(Swift 約 46 萬行、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 文件;macOS runner 排隊問題可參考 GitHub Actions macOS runner 為什麼總在排隊。
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 管理。高頻 CI 的資源隔離可參考 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 限制)。