限时优惠

为什么 Flutter iOS CI 在云 Mac 上会变慢?三层缓存失效是关键原因

CI Flutter · iOS 构建
2026-06-04 约 15 分钟阅读

本文只讲一件事:Flutter iOS CI 是「缓存失效传播问题」,不是三个并列系统各自变慢。主矛盾在 CocoaPods(网络 + 解析,常占时间上限约 70%),其次才是 Xcode DerivedData cache;Flutter 工具链缓存多为边缘优化。读完第 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 字符串更重要。
Flutter iOS CI 在云 Mac 上的构建与缓存调优
封面为场景示意;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 的真实瓶颈排序(全文统帅句)

后文所有章节服从同一判断——不是三层同等重要,而是权重不对称:

时间上限(先优化谁):
  CocoaPods(网络 + 解析)  🔥🔥🔥  常决定 ~70% 上限
        ↓
  DerivedData(编译增量)   🔥🔥    决定热构建能否进 6 min 档
        ↓
  Flutter cache(工具链)   🔥      主要省首轮 pub / 引擎下载

总判断(可引用):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 节 的「优化优先级」一致:先救起点):

  [起点 · 主矛盾]  CocoaPods 未命中 / 私库慢
           │  Pod 树重建、Specs 拉取
           ▼
  [传播]           DerivedData 被迫冷编译(全量 xcodebuild)
           │
           ▼
  [末端]           IPA 产出慢(表象往往误怪到 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 部署 · CocoaPods 环境变量 · 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,否则 Benchmark 没有可比性。

3. 与双 Agent 同机:只谈资源边界

昨日文《云 Mac 双 AI Agent:Claude Code + Codex 隔离》解决的是两个 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
# 持久化:~/.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/ 目录。对 CocoaPods CI optimization 来说,能把「慢」定位到网络还是解析,比再叠一层 wrapper 脚本更有用。

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 见 Runner 磁盘治理文

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 版本
4deriveddata 🔥🔥$DERIVED_DATA_PATHXcode + Flutter + lockfiles
# .github/workflows/flutter-ios.yml(结构示意,macos-latest 或 self-hosted 云 Mac)
jobs:
  build-ios:
    runs-on: macos-14   # 或 runs-on: [self-hosted, cloud-mac]
    steps:
      - uses: actions/checkout@v4

      # ── Restore(顺序:Pub → Pods Specs → Pods 树 → 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(与 restore 对称,失败也建议 save 部分层)──
      - 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') }}

restore-keys 可做前缀回退,但须在日志标明部分命中,避免半残 DerivedData 被当成成功优化。构建完成后的签名链见 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 同机怎么配合?

双 Agent 文管 worktree 隔离;本文管 CI 缓存根。同机时 CI 用独立 DERIVED_DATA_PATH,避免与 Agent 共 DerivedData(第 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 上完成双 Agent 验收,可用同一台机加一条夜间 Flutter Pipeline:第 1 天记 cold 基线,第 2 天按 第 7 节 打开四层 restore,第 3 天对照 第 2 节 Benchmark 看 hot 是否进入 ~6 min 区间。若 pod install 仍慢,先查私库网络,再考虑升配。

需要对照 SSH 开通与 M4 规格时:租 Mac 开通验收清单 · 规格与租期 · 配置方案