限時優惠

為什麼 Flutter iOS CI 在雲 Mac 上會變慢?三層快取失效是關鍵原因

CI Flutter · iOS 建置
2026-06-04 約 15 分鐘閱讀

本文只講一件事:Flutter iOS CI 是「快取失效傳播問題」。主矛盾在 CocoaPods,其次 DerivedData。讀完第 0 節與 0.1 節即可決定先改哪一層。

30 秒讀完(按優先級)

  1. 主矛盾: Pods(網路 + 解析)> DerivedData(編譯增量)> Flutter cache(工具鏈)——先救 Pods,再談增量編譯。
  2. 單因果: 不是「三層都慢」,而是上一層快取失效 → 迫使下一層重做(傳播鏈見 §1)。
  3. 唯一原則(§0):快取綁定 lockfile / SDK 版本,不綁日期。
  4. 忙則只讀: §0 + §0.1 + §4 CocoaPods;熱建置仍慢再讀 §5
  5. §7 Pipelinerestore 順序比 key 字串更重要。
雲 Mac 上 Flutter iOS CI 建置與快取調校
封面為場景示意;Benchmark 數據見下文標註的測試條件。

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 getpod installxcodebuild,各記 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 getpod installxcodebuild(或 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 --verboseflutter 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)
1pub 🔥PUB_CACHEpubspec.lock + Flutter SDK
2–3pods 🔥🔥🔥~/.cocoapods + ios/PodsPodfile.lock + CP version
4deriveddata 🔥🔥$DERIVED_DATA_PATHXcode + 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 開通驗收清單 · 規格與租期 · 配置方案