限时优惠

xcodebuild 在 CI 为什么比本地慢 2-3 倍?

CI Flutter iOS CI
2026-06-08 约 12 分钟阅读

这是一篇面向 Flutter iOS 团队的实战排查文:用阶段日志定位 xcodebuild 的真实瓶颈,再决定是继续 GitHub Actions 托管机还是迁移到自托管 Runner。

Flutter CI 中 xcodebuild 与本地编译耗时对比
本地快、CI 慢:差距多半出在缓存和编译模式,而不是机器芯片本身

本文要点

  1. 本地快,是因为有缓存、增量编、常跑 Debug;CI 慢,是因为常做 Release 全量编
  2. 慢的不只是 xcodebuild 本身——前面的 pod install 也会吃掉大量时间。
  3. 想让 CI 接近本地速度:留住 DerivedData 和 Pods 目录,别每次清空重来。

先对齐:你比的是不是同一件事

很多人吐槽「CI 慢」,其实对比并不公平。本地开发时,你多半是这样:

  • 改几行 Dart,Xcode 只增量编译动到的文件;
  • 昨天编过的原生依赖还在 DerivedData 里,不用重编;
  • 跑的是 Debug,优化级别低、出包快。

而 CI 流水线里,典型做法是:

  • 每次 checkout 一份干净的工作区
  • flutter build ipaxcodebuild archive,走 Release
  • 托管 Runner 上缓存往往存不住,等于每次从零编。

所以「本地 5 分钟 vs CI 15 分钟」并不反常——2–3 倍差距在 Flutter iOS 团队里非常普遍。问题不是「谁对谁错」,而是搞清楚慢在哪一段,再对症下药。

CI 比本地慢的 5 个常见原因

按出现频率排序,通常是下面五条(你可以当成「嫌疑人名单」):

  1. 没有编译缓存(DerivedData 冷启动)——本地机器上攒了几天的编译产物,CI 每次清空,所有 .m / .swift 文件从头编。
  2. Release 全量编——CI 要上架或出 IPA,必须 Release;比本地 Debug 慢一截是正常的。
  3. pod install 每次都重装——日志里一片 Installing …,几分钟就没了,还没轮到 xcodebuild。
  4. 机器内存不够触发 swap——16GB 跑大型 Flutter 工程,xcodebuild 峰值吃满内存,磁盘 swap 会把时间拉爆。
  5. 排队和多余步骤——单台 Mac Runner 上多条 Job 排队;或 workflow 里每次都 brew installflutter upgrade

注意:前三条加起来,往往就能解释「为什么慢 2–3 倍」;后两条是「明明缓存配好了还是慢」时再查。

打开日志:时间到底花在哪

别只看 Actions 页面右上角的总时长。点进 build-ios Job,看每个 step 的耗时,心里可以画一条简单时间线:

步骤 第一次(冷) 第二次(热) 本地开发对比
flutter pub get1–3 分钟十几秒本地也有缓存,差不多
pod install4–9 分钟半分钟–2 分钟本地很少每次重装
xcodebuild / build ipa8–15 分钟4–8 分钟本地 Debug 增量常 <5 分钟
签名上传1–3 分钟1–2 分钟本地手动 archive 也有

一眼判断法:如果 pod install alone 就超过 5 分钟,先别怪 xcodebuild,去修 Pods 缓存;如果 pod 很快但 xcodebuild 仍超 10 分钟,重点查 DerivedData 有没有命中。更系统的缓存模型可以看姊妹文 为什么 Flutter iOS CI 在云 Mac 上变慢

xcodebuild 之前:pod install 这条坑

不少同学盯着 xcodebuild,却忽略了它前面还有一道关。CocoaPods 在 CI 里慢,通常就三个原因:

  • 没提交 Podfile.lock——每次重新算依赖,时间不可控。
  • 没缓存 ios/Pods 目录——托管 Runner 跑完就删,下次又从 CDN 拉。
  • 多跑了 pod repo update——CI 里一般不需要,锁文件已经钉死版本。

改法很具体:CI 里用 pod install --deployment(只按 lock 文件装,不重新解析),并把 Pods 目录缓存在同一台机器的 SSD 上。自托管 Mac mini 比 GitHub 托管 Runner 省事——缓存直接落本地盘,不用先打包上传到 GitHub 再下载(那一步本身就要几分钟)。

xcodebuild 本身:DerivedData 有没有留住

这是「比本地慢 2–3 倍」的头号原因。本地 Xcode 会把编译中间产物放在 DerivedData 里,你改一行代码,只重新编译受影响的目标;CI 如果每次 Job 都把 DerivedData 指到临时目录,或者干脆 flutter clean,就等于强迫全量重编

怎么确认缓存没命中?看 xcodebuild 日志里有没有大量 CompileCSwiftCompile 针对你没改过的 Pod。如果 Firebase、某地图 SDK 每次都被重编,说明 DerivedData 没留住。

实用做法(自托管 Runner 上):

  • DERIVED_DATA_PATH 固定到用户目录下某个路径,不要在 step 之间删
  • Podfile.lock 的 hash 做子目录名,换依赖时才换缓存桶;
  • CI 里不要随便 flutter clean,除非你真的换了 Flutter 大版本。

托管 macos-latest 也能用 actions/cache 存 DerivedData,但大包上传下载很慢,且容易超大小限制——所以很多团队迁到自托管后,xcodebuild 时间能直接砍半,靠的就是「缓存留在机器上」。Runner 怎么搭、缓存目录放哪,见 Mac mini 自托管 Runner 搭建指南

能立刻见效的改法(按优先级)

不用一次改完,按下面顺序做,通常每一步都能看见收益:

  1. workflow 里加 concurrency——同一分支新 push 取消旧构建,少排队、少重复编。(见下方代码片段)
  2. 测试和分析放 Linux——flutter test 别占 Mac 时间,失败了也别启动 xcodebuild。
  3. 持久化 Pods + DerivedData——自托管机器上固定路径;托管 Runner 用 actions/cache 并缩小缓存键粒度。
  4. CI 只编要发的那个 flavor——别在流水线里扫 6 个 flavor,实际只上传 1 个包。
  5. 固定 Xcode / Flutter 版本——别在 CI 里 brew upgradeflutter upgrade,避免缓存整体失效。
# 放在 workflow 顶部,减少排队和重复构建
concurrency:
  group: ios-${{ github.ref }}
  cancel-in-progress: true

# build-ios Job 里:尽量别 clean,固定 DerivedData
env:
  FLUTTER_BUILD_MODE: release
  # 自托管示例:DerivedData 持久路径(按你机器目录调整)
  DERIVED_DATA_PATH: /Users/ci/ci-cache/DerivedData/${{ hashFiles('ios/Podfile.lock') }}

更完整的 Job 拆分(Linux 测、Mac 编)和标签路由,见专题 Hub Flutter + GitHub Actions + Mac mini 架构总览。官方对 workflow 的说明见 GitHub Actions 文档

对照清单:你的 CI 中了哪几条

对着自己的仓库打勾,勾得越多,xcodebuild 越可能比本地慢 2–3 倍:

  • ☐ 每次 CI 都跑 Release / IPA,本地对比的是 Debug
  • ☐ Job 里写过 flutter clean 或删 DerivedData
  • pod install 日志里每次大量 Installing
  • ☐ 使用 GitHub 托管 Runner,没用本地 SSD 缓存
  • ☐ 16GB 机器同时跑多条 Job 或模拟器
  • ☐ 改 README 也触发了 iOS 全量构建
  • ☐ Xcode 版本和本地不一致

勾了前三条,优先修缓存;勾了后两条,优先修 workflow 触发条件和机器规格。磁盘满了也会导致「越编越慢」,可参考 Runner 磁盘清理

常见问题

本地 5 分钟、CI 15 分钟,正常吗?

没做缓存、CI 跑 Release 的前提下,正常。目标不是和本地 Debug 一样快,而是让第二次 CI 构建接近你本地改一行代码的速度——热缓存到位后,8–10 分钟以内很常见。

换更快的 Mac 芯片能缩短 xcodebuild 吗?

有帮助,但不如缓存来得猛。M4 比旧 Intel 快一截,但如果每次冷编,照样 15 分钟;留住 DerivedData,往往比换芯片多省 5–8 分钟。

actions/cache 够了,还要自托管吗?

小项目可以。Flutter + 多 Pod 的工程,DerivedData 经常几个 GB,cache 上传下载反而添乱。独占一台 Mac mini、缓存放本地盘,维护成本更低,xcodebuild 也更稳定。

怎么判断优化生效了?

同一 commit 连续跑两次 CI:第二次 pod install 应明显变短,xcodebuild 日志里「重编未改动的 Pod」应消失。总时长从 20+ 分钟掉到 10 分钟以内,说明路走对了。

想让 CI 里的 xcodebuild 接近本地?

关键不是买「最快的云」,而是缓存能留在机器上kvmboot 云端 Mac mini M4 提供独占硬盘和固定 Xcode 环境,适合当 Flutter CI 的自托管 Runner——日租跑两轮对比冷/热构建,看 xcodebuild 能否从 15 分钟压到 8 分钟,再决定是否月租。

查看 Mac mini 套餐 · 配置方案