30 秒讀完(按優先級)
- 主矛盾: Pods(網路 + 解析)> DerivedData(編譯增量)> Flutter cache(工具鏈)——先救 Pods,再談增量編譯。
- 單因果: 不是「三層都慢」,而是上一層快取失效 → 迫使下一層重做(傳播鏈見 §1)。
- 唯一原則(§0):快取綁定 lockfile / SDK 版本,不綁日期。
- 忙則只讀: §0 + §0.1 + §4 CocoaPods;熱建置仍慢再讀 §5。
- §7 Pipeline:restore 順序比 key 字串更重要。
0. 快取設計的唯一原則
在展開 CocoaPods、DerivedData 或 PUB_CACHE 之前,先統一一條架構原則——後文所有「key 怎麼寫」都服從它:
CI 快取不是「存檔案」,而是「綁定建置確定性」。
pubspec.lock/Podfile.lock變化 → cache miss → 正常- Xcode 或 Flutter SDK 升級 → cache miss → 正常
- Job 執行時間、日期、隨機後綴 → 不應單獨導致 miss
違反這條原則的典型症狀:每天都像冷啟動、熱建置和冷啟動一樣慢——說明你在用「時間」或「全量清空」管理快取,而不是用「版本 + lockfile」管理確定性。
0.1 Flutter iOS CI 的真實瓶頸排序(全文統帥句)
後文所有章節服從同一判斷——不是三層同等重要,而是權重不對稱:
Time ceiling (optimize in this order):
CocoaPods (network + resolve) 🔥🔥🔥 often ~70% of ceiling
↓
DerivedData (compile increment) 🔥🔥 hot builds → ~6 min tier
↓
Flutter cache (toolchain) 🔥 mainly first-run pub / engine
總判斷(可引用): Flutter iOS CI 的慢,通常不是三層問題並列,而是Pods 先卡住時間上限;Pods 未命中時,DerivedData 做得再好也只能在下游「空轉」。
閱讀路徑: 只有 10 分鐘 → §0 + 本節 + §4;cold 已進 15 min 但 hot 仍慢 → 再加 §5;§6 留給映像烘焙與首輪 pub get。
1. 單因果模型:快取失效如何傳播到 IPA
本文不講三個獨立子系統,只講一條傳播鏈——失效從上游往下游強迫重做。調優時先問:斷點在上游還是下游?
flutter build ipa 日誌若只有 Running pod install...,先用 §0.1 判斷:多數情況應優先查 Pods 快取與網路,而不是先調 Xcode 編譯參數。分段打點仍不可少:pub get → pod install → xcodebuild,各記 wall time。
1.1 傳播鏈(起點 → 末端)
按因果方向閱讀(與 §0.1 的「優化優先級」一致:先救起點):
[start · primary] CocoaPods miss / slow private specs
│ Pod tree rebuild, Specs fetch
▼
[propagate] DerivedData forced cold (full xcodebuild)
│
▼
[end] Slow IPA (often blamed on Flutter/Dart)
反向觸發也成立:pubspec.lock 變更 → 外掛變 → pod install 重算 → DerivedData 失效。因此 Pipeline 裡 restore 仍建議 Pub 在前,但排障與投入時間按 §0.1 權重:先 Pods,再 DerivedData,最後 Flutter cache。
可引用結論: 約 90% 的「Flutter CI 慢」不是編譯器慢,而是傳播鏈上游失效。典型:DerivedData 被清空時,即便 Pods 已命中,xcodebuild 仍像冷啟動——那是傳播段的損失,不是 Dart 的問題。
官方入口: Flutter iOS deployment · CocoaPods environment · Xcode Build Settings.
2. Benchmark:驗證 §0.1 節權重(參考區間)
下表用於證明先 Pods、後 DerivedData 的投入順序——不是三層平均用力。數據來自 kvmboot 工單典型中型 Flutter 專案(約 40 Pod、Release IPA、雲 Mac M4),請用同 commit 自建基線。
| 優化階段 | 權重 | cold | hot | 說明 |
|---|---|---|---|---|
| 無快取 | — | ~28 min | ~28 min | 傳播鏈全斷;熱冷無差別 |
| 僅 Pods + Specs cache | 🔥🔥🔥 | ~19 min | ~12 min | 最大單步降幅(28→19);先做這個 |
| + DerivedData cache | 🔥🔥 | ~11 min | ~6 min | 熱建置關鍵;Pods 命中後才明顯 |
| + Flutter pub/引擎 cache | 🔥 | ~10 min | ~5 min | 邊緣優化;省首輪 pub get |
若 cold 仍 >20 min,先別動 DerivedData,回到 §4 查 Pods/私庫;若 cold 已 ~11 min 但 hot 仍 >12 min,再查 §5 restore 與 DERIVED_DATA_PATH。
如何自測: 在 Pipeline 裡為每個階段加 /usr/bin/time -l 或 Actions 的 step summary,輸出四行即可——pub get、pod install、xcodebuild(或 flutter build ipa 內的 Xcode 段)、總 wall time。對比「無快取 / 僅 Pods / Pods+DerivedData」三檔時,務必固定同一 commit 與同一 FLUTTER_VERSION。
3. 與雙 Agent 同機:只談資源邊界
2026-06-03 的伴侶文 雲 Mac 雙 AI Agent:Claude Code + Codex 隔離(2026-06-03) 解決的是兩個 CLI 不寫壞同一 worktree。疊加 Flutter iOS 夜間 CI 時,額外注意三點(技術區不展開租期):
- 分快取根: CI 用獨立
DERIVED_DATA_PATH,不與 Agent 共用~/Library/Developer/Xcode/DerivedData。 - 分排程:
xcodebuild與 Agent 索引並行會把 swap 打滿,熱建置反而更慢;16GB 宜「Agent 白天 + CI 夜間」。 - 禁交叉操作: Agent 不要在 CI 的
ios/Pods上試pod update。
4. CocoaPods CI 優化 🔥🔥🔥
主矛盾層。 約 70% 的 Flutter iOS build slow 卡在 pod install(網路 + Specs 解析)。本節決定你能否從 28 min 進到 ~19 min 檔——沒做好 DerivedData 救不回來。按 §0:key 綁 Podfile.lock + CocoaPods 版本。
4.1 環境變數與持久化路徑
export CP_HOME_DIR="${CP_HOME_DIR:-$HOME/.cocoapods}"
export COCOAPODS_PARALLEL_CODE_DOWNLOAD=true
# persist: ~/.cocoapods/repos + ~/Library/Caches/CocoaPods
私庫 / 二進位 Pod:Runner 與 Git 源同區域;認證走 .netrc 或 CI secret,勿寫進 Podfile。託管 vs 自建 Runner 的私庫策略見 Bitrise 與雲 Mac Runner 決策文.
4.2 Pods/ 樹:快取還是入庫
- 常見: CI 快取
ios/Pods,key =hashFiles('ios/Podfile.lock')。 - 合規向: 提交
Pods/,CI 只跑pod install --deployment。
避免無差別 pod update——它會讓 lockfile 漂移,§0 的確定性被破壞,cache 永遠 miss。
若 flutter build ios 在內部隱式呼叫 pod,建議 CI 裡顯式先 cd ios && pod install --verbose 再 flutter build ipa:日誌分段更清晰,也避免 Flutter 與獨立 pod 步驟爭搶同一份未恢復的 Pods/ 目錄。
5. Xcode DerivedData cache 🔥🔥
傳播段。 Pods 命中後,DerivedData 才決定 hot build 能否進 ~6 min。按 §0:key 綁 Xcode + Flutter + lockfiles;禁止跨版本複用。若 §4 仍慢,本節不是優先項。
export DERIVED_DATA_PATH="/var/ci/derived/flutter-${FLUTTER_VERSION}-xcode-${XCODE_VERSION}"
xcodebuild -workspace ios/Runner.xcworkspace -scheme Runner \
-configuration Release -derivedDataPath "$DERIVED_DATA_PATH" \
-destination 'generic/platform=iOS' build
清理按 Tier 做,不要每 Job rm -rf DerivedData/*:Job 級只清 /tmp;日級清未命中子目錄;週級在 major 升級後廢棄整 key。磁碟配額與 inode 見 雲 Mac Runner 磁碟與 inode 治理文.
6. Flutter 工具鏈快取 🔥
邊緣層。 PUB_CACHE 與 SDK bin/cache 主要省首輪下載;Benchmark 上通常只有 1–2 min 量級收益。pubspec.lock 變更會向上游觸發 pod 重算——屬 §1 傳播鏈,不是單獨故障。
跨 Job 勿優先恢復 build/ios;iOS 建置固定在裸 macOS / 雲 Mac(見 Docker 排障文.
7. GitHub Actions macOS runner:按 §0.1 權重落地的 Pipeline
把 §4–§6 合成可運行 YAML。restore 順序(Pub → Pods → DerivedData)服務於傳播鏈;排障與改 key 的精力按 §0.1:先 Pods,再 DerivedData。
| 順序 | 快取塊 | 路徑 | key 因子(§0) |
|---|---|---|---|
| 1 | pub 🔥 | PUB_CACHE | pubspec.lock + Flutter SDK |
| 2–3 | pods 🔥🔥🔥 | ~/.cocoapods + ios/Pods | Podfile.lock + CP version |
| 4 | deriveddata 🔥🔥 | $DERIVED_DATA_PATH | Xcode + Flutter + lockfiles |
# .github/workflows/flutter-ios.yml (structure; macos-latest or self-hosted cloud Mac)
jobs:
build-ios:
runs-on: macos-14 # or runs-on: [self-hosted, cloud-mac]
steps:
- uses: actions/checkout@v4
# ── Restore (order: Pub → Pods Specs → Pods tree → DerivedData) ──
- name: Restore Pub Cache
uses: actions/cache/restore@v4
with:
path: ${{ env.PUB_CACHE }}
key: pub-${{ hashFiles('pubspec.lock') }}-flutter-${{ env.FLUTTER_VERSION }}
- name: Restore CocoaPods Specs
uses: actions/cache/restore@v4
with:
path: |
~/.cocoapods
~/Library/Caches/CocoaPods
key: pods-specs-${{ env.COCOAPODS_VERSION }}
- name: Restore Pods Tree
uses: actions/cache/restore@v4
with:
path: ios/Pods
key: pods-tree-${{ hashFiles('ios/Podfile.lock') }}
- name: Restore DerivedData
uses: actions/cache/restore@v4
with:
path: ${{ env.DERIVED_DATA_PATH }}
key: dd-${{ env.XCODE_VERSION }}-${{ env.FLUTTER_VERSION }}-${{ hashFiles('pubspec.lock', 'ios/Podfile.lock') }}
# ── Build ──
- name: Flutter Pub Get
run: flutter pub get
- name: Pod Install
run: cd ios && pod install --verbose
- name: Flutter Build IPA
run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
# ── Save (mirror restore; save costly layers even on failure) ──
- name: Save Pub Cache
uses: actions/cache/save@v4
if: always()
with:
path: ${{ env.PUB_CACHE }}
key: pub-${{ hashFiles('pubspec.lock') }}-flutter-${{ env.FLUTTER_VERSION }}
- name: Save Pods + DerivedData
uses: actions/cache/save@v4
if: always()
with:
path: |
~/.cocoapods
~/Library/Caches/CocoaPods
ios/Pods
${{ env.DERIVED_DATA_PATH }}
key: dd-${{ env.XCODE_VERSION }}-${{ env.FLUTTER_VERSION }}-${{ hashFiles('pubspec.lock', 'ios/Podfile.lock') }}
構建完成後的簽名鏈見 codesign / Notarization 專文,本文不重複。
常見踩坑:先 pod install 再 restore Pods 樹——等於白恢復;build 失敗就 skip save——會丟失已生成的 DerivedData,下次仍是冷啟動。Actions 上對耗時層使用 if: always() 的 save,與 restore 對稱,才能穩住 hot build 曲線。
8. 磁碟與 inode(只列工程要點)
三層快取疊加後,256GB SSD 可能在數天內達到 60% 佔用,且inode 往往先於容量觸頂。建議:CI 使用者 80% 容量 / 85% inode 告警;大快取放獨立掛載點;月租 Runner 做 Tier-1 清掃。發版衝刺型日租機可在租期結束直接丟棄實例,見 臨時構建矩陣文.
du -sh ~/Library/Developer/Xcode/DerivedData ~/.cocoapods "${PUB_CACHE:-$HOME/.pub-cache}" 2>/dev/null
df -h .; df -i .
9. 驗收與排障(按 §0.1 順序)
第一步(🔥🔥🔥):pod install lock 不變時 <90s,cold 進入 ~19 min 檔。第二步(🔥🔥):同 commit 熱建置 P95 較 cold 縮短 ≥40%,目標 ~6 min。第三步(🔥):首輪 pub get 是否可省略。不要跳過第一步直接調 DerivedData。
| 症狀 | 高機率根因 | 首選動作 |
|---|---|---|
| 熱 ≈ 冷,全程 20+ min | DerivedData 每 Job 被清 / 路徑未固定 | 查 -derivedDataPath 與 restore 是否執行 |
| 僅 pod 階段慢 | Specs CDN / 私庫認證 | CP_HOME_DIR、.netrc、區域 |
| 偶發連結錯誤 | 跨 Xcode 版本複用 DerivedData | key 加入 Xcode 版本,廢棄舊目錄 |
| SSH 卡頓 | 磁碟滿 / inode 耗盡 | df -h; df -i + 磁碟治理文 Tier 清理 |
10. FAQ(搜尋常見問法)
10.1 為什麼 Flutter iOS CI 在雲 Mac / GitHub Actions 上特別慢?
多數是 CocoaPods 快取未命中(網路、私庫、Specs),傳播導致 DerivedData 冷編譯。先按 §0.1 權重查 Pods,再查 DerivedData——不要先懷疑 Dart 編譯器。
10.2 Flutter iOS build slow:應該先優化 CocoaPods 還是 DerivedData?
先 CocoaPods。 Benchmark 顯示僅 Pods cache 即可從 ~28 min 降到 ~19 min;DerivedData 在 Pods 命中後才主導 hot build(~12 min → ~6 min)。
10.3 只快取 Xcode DerivedData 夠嗎?
不夠。DerivedData 是傳播段優化;pod install 仍可能占滿冷啟動。見 §0.1 與 §2 表第一行 🔥🔥🔥。
10.4 GitHub Actions macOS runner 上 Flutter 快取怎麼設?
見 §7:restore 順序 Pub → Pods → DerivedData;save 用 if: always()。key 綁 lockfile/SDK,不綁日期(§0)。
10.5 和雙 Agent 同機怎麼配合?
2026-06-03 雙 Agent 文管 worktree 隔離;本文管 CI 快取根。同機時 CI 用獨立 DERIVED_DATA_PATH(§3)。
11. 結論
Flutter iOS CI 不是「Flutter、Pods、Xcode 三個系統各修各的」——而是一條快取失效傳播鏈,時間上限由 CocoaPods(🔥🔥🔥)→ DerivedData(🔥🔥)→ Flutter cache(🔥) 依次決定。先按 §4 把 Pods 打進 ~19 min 檔,再用 §5 打 hot build,§6 與 §7 收尾。記住 §0.1:Pods 決定你能快多少,DerivedData 決定你能不能把熱建置穩住。
用 48 小時驗證這套 iOS CI cache strategy
若你已在雲 Mac 上完成雲 Mac 雙 AI Agent:Claude Code + Codex 隔離(2026-06-03),可用同一台機加一條夜間 Flutter Pipeline:第 1 天記 cold 基線,第 2 天按 §7 打開四層 restore,第 3 天對照 §2 Benchmark 看 hot 是否進入 ~6 min。若 pod install 仍慢,先查私庫網路,再考慮升配。
需要對照 SSH 開通與 M4 規格時:租 Mac 開通驗收清單 · 規格與租期 · 配置方案