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 的真实瓶颈排序(全文统帅句)
后文所有章节服从同一判断——不是三层同等重要,而是权重不对称:
时间上限(先优化谁):
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 get → pod install → xcodebuild,各记 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 get、pod install、xcodebuild(或 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 --verbose 再 flutter 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 节) |
|---|---|---|---|
| 1 | pub 🔥 | PUB_CACHE | pubspec.lock + Flutter SDK |
| 2–3 | pods 🔥🔥🔥 | ~/.cocoapods + ios/Pods | Podfile.lock + CP 版本 |
| 4 | deriveddata 🔥🔥 | $DERIVED_DATA_PATH | Xcode + 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 开通验收清单 · 规格与租期 · 配置方案