← 返回技术博客

为什么 GitHub Actions iOS CI 在 Mac 上这么慢?self-hosted runner 才是真正的加速点

GitHub Actions iOS CI:Mac mini self-hosted runner 与持久 DerivedData 缓存架构
托管 macOS Runner 每次冷启动;独享 Mac mini self-hosted runner 让 DerivedData 跨 job 存活。

很多团队在 GitHub Actions iOS CI 上遇到的第一个困惑是:明明已经是 Apple Silicon,为什么一条 Archive 还是要等半小时? 第一反应往往是换更快的 Mac、加更多核——但打开 Actions 日志你会发现,真正吃掉时间的常常是排队、依赖重装、冷 DerivedData、签名与上传,而不是 xcodebuild 里那几分钟编译。换句话说,GitHub Actions iOS CI 在 Mac 上慢,慢在运行模型,不是慢在芯片型号。

这篇只讲一件事:self-hosted macOS runner 为什么是 iOS 流水线里性价比最高的加速杠杆。我们会用一台独享 Mac mini M4 上的真实分工——持久磁盘、固定席位、可预测月费——对照托管 Runner 的 ephemeral 环境,说明典型中等规模工程如何把全链路从约 28 分钟压到约 9 分钟(以下为站内多组客户样本的中位数,非 SLA 承诺,请用自家仓库验证)。若你已在看节点 TCO,可先读 六地 Runner 横评;Flutter 与 Fastlane 细节分别见 Flutter iOS CIFastlane TestFlight。算力账单的大背景可对照 AI 算力基础设施战争——开发者侧最该优化的,往往是独享 Runner 席位而不是再买一个更贵的托管分钟包。

1)先排除三个误读:慢 ≠ M 芯片不行、慢 ≠ workflow 写错一行

在改 YAML 之前,建议先把日志里的耗时拆成四段:queued(排队)bootstrap(拉代码 + 装依赖)build(编译与 Archive)release(签名 + 上传)。托管 macOS Runner 上,bootstrap 与 build 往往被「每次 job 一张白纸」放大——pod install 从零拉 spec repo、SPM 重新 resolve、DerivedData 目录为空,Archive 只能全量编译。排队则与组织内 macOS 并发配额、高峰时段共享池有关,和你在本地 M4 上「秒编」的体验完全不是同一套资源。

第二个误读是「加 runs-on: macos-latest 就等于有了一台自己的 Mac」。托管 Runner 按 GitHub Actions 计费规则 消耗 macOS 分钟,环境在 job 结束后销毁;你无法指望 ~/Library/Developer/Xcode/DerivedData 在 PR #412 里还在。第三个误读是把优化全押在「换 Xcode 版本」或「关几个编译警告」——这些有用,但在冷缓存面前通常只省几分钟。

自测 10 分钟: 在 Actions 里加一步 du -sh ~/Library/Developer/Xcode/DerivedData。若每次 job 都是 0 或极小,你的主要矛盾就是没有持久 Runner 磁盘,而不是 scheme 配置。

2)28 分钟到底花在哪:托管 Runner 的四段账单

我们汇总了十余个 Swift/UIKit 中等规模 App(单 scheme、CocoaPods、日更 main)在托管 macOS 上的 P50 耗时,大致如下。数字会因仓库体积、网络、是否用缓存 action 而波动,但结构比例很稳定:

阶段 典型耗时(托管) 主要成因
queued 3–12 分钟 组织 macOS 并发、高峰共享池、多仓库抢席位
bootstrap 6–10 分钟 pod install、SPM fetch、无 DerivedData
build / archive 8–14 分钟 全量编译;偶发 Xcode 首次索引
sign + upload 3–6 分钟 证书导入、ASC 上传、处理等待

注意:build 往往不是最长的一段。当 bootstrap 每次从零开始时,你会感觉「Mac 很慢」——其实是重复劳动。这也解释了为什么有些团队本地 Archive 只要 4 分钟,CI 却要 25 分钟:本地有几个月沉淀的 DerivedData 和 Pods,CI 没有。

3)真正的加速点:独享 self-hosted runner + 持久磁盘

self-hosted macOS runner 的核心收益不是「CPU 更快」,而是同一台机器、同一块 SSD、同一个 macOS 用户反复跑 job:actions-runnerlaunchd 守护,job 之间不销毁磁盘。注册与维护方式见 GitHub 官方 self-hosted runner 文档

在 Nuvcloud 等云端 Mac mini 上部署时,你还额外得到:机房稳定出口、7×24 在线、可按日租试跑再转月租——比把同事的 MacBook 当 Runner(合盖就断、系统升级就炸)可靠一个数量级。混合办公背景下,Windows 侧写代码、Mac 只负责 CI 的分工,可参考 告别 Xcode 卡顿

GitHub Actions iOS CI · self-hosted 目标架构

git push / PR
Linux job:测试 · lint · Android
GitHub · workflow 调度
runs-on: [self-hosted, macos, ios-ci]
Mac mini M4 · 独享 Runner
无排队 · 持久 DerivedData/Pods
Archive → TestFlight / 制品库
签名材料跨 job 复用

跨 job 保留(不要在工作流里 rm -rf)

  • DerivedData
  • ~/Library/Caches/CocoaPods
  • .build / SPM
  • Match 钥匙串
  • npm / bundler

把 Runner 放在独享 Mac mini 上时,queued 段往往直接归零——你买的是固定席位,不是和别人抢托管池。bootstrap 在第二次 job 起大幅下降:pod install 命中本地 spec 缓存,SPM 包已在 .build 里。build 走增量编译,P50 可从 10+ 分钟降到 3–5 分钟。若签名与 Match 也持久化(见 Fastlane 文),release 段同样更稳。

4)缓存策略:DerivedData、CocoaPods、SPM 与 actions/cache 的分工

self-hosted 上的第一原则:能留在磁盘上的就不要每个 job 重新下载。推荐分层如下。

  • DerivedData:固定在 Runner 用户主目录,禁止在 workflow 末尾清理。大仓库可设 DERIVED_DATA_PATH 指向独立目录并监控容量。
  • CocoaPods:保留 Pods/~/Library/Caches/CocoaPods;仅 Podfile.lock 变更时做完整 install。
  • SPM:保留 .buildSourcePackages;CI 上与本地相同的 resolved 文件。
  • actions/cache:仍可用于备份超大 DerivedData 到 GitHub 缓存服务,但在持久 Runner 上优先级低于本地盘——网络拉缓存往往比本地增量编译还慢。

Apple 对 CI 缓存的官方讨论分散在 Xcode 文档 与社区最佳实践中;对 iOS 团队而言,实操结论很简单:谁持有磁盘,谁掌握 P50 编译时间

5)workflow 与 Runner 池:label 设计、并发与 1 台 vs 多台

在 workflow 里用明确 label 把 iOS job 打到你的 Mac mini 上,避免和其它实验性 Runner 混跑:

jobs:
  ios-release:
    runs-on: [self-hosted, macos, ios-ci]
    concurrency:
      group: ios-release-${{ github.ref }}
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v4
      - name: Build archive
        run: xcodebuild -scheme MyApp -configuration Release archive ...

并发:Archive 与导出 IPA 建议同一 Runner 上release 类 job 串行concurrency 设为 1),避免两个 xcodebuild 抢签名与 DerivedData 锁。PR 检查若要与 release 并行,可用第二台 Mac mini 或给 PR job 单独 label(如 ios-pr)。

1 台 vs 多台:日更单 App、一条 release 流水线,一台 M4 通常够用;多 App、多 scheme 并行或「PR 检查 + 夜间全量」同时跑,按峰值加第二席位,比盲目加托管分钟数更可控。节点选型与月租对照见 Runner TCO 横评 与日本/新加坡专文。

6)M4 16GB vs 24GB:内存、磁盘与何时扩容

mac mini ci 而言,内存决定能否并行多个编译进程,磁盘决定缓存能留多久。经验法则:

配置 适合 注意
M4 16GB · 默认盘 单 scheme、日更、Pods 中等、一条 release job 监控 DerivedData;超大 Asset Catalog 可能吃满内存
M4 24GB · 扩容盘 Flutter + 原生混编、多 target、并行 PR + Archive 为 DerivedData + Pods + 制品预留 ≥200GB 余量

若磁盘长期 >85%,不要靠「删缓存提速」——应扩容或拆 Runner,否则你会回到托管 Runner 式的冷启动噩梦。

7)Before / After:同一仓库托管 vs self-hosted(样本中位数)

阶段 托管 macOS Runner Mac mini self-hosted
queued 7 min 0 min
bootstrap 9 min 2 min
build / archive 11 min 4 min
sign + upload 5 min 3 min
合计 ~28 min ~9 min

第二次及以后的 job,bootstrap 与 build 还会再降一截——这就是持久 Runner的复利。请用自家仓库跑 48 小时 A/B,不要直接套用表中数字签 SLA。

8)48 小时验证清单:从日租到月租的最短路径

  1. 在目标节点开 48h 日租 Mac mini(日本/新加坡/美西等按 Git 远端就近选,见 定价页)。
  2. 按官方文档注册 self-hosted runner,打 label ios-ci,确认 launchd 守护进程在 reboot 后仍在线。
  3. 跑一条无缓存清理的 release workflow,记录四段耗时;再跑第二条 PR,对比 bootstrap 是否下降。
  4. 检查 DerivedData 目录体积是否随 job 增长后趋于稳定(增量编译生效)。
  5. 对比过去 30 天托管 macOS 分钟账单:若 M × 单价 > 月租 且 P50 耗时满意,转月租锁席位。
先说清楚: 这篇不教 OpenClaw 怎么装,也不把六地横评那张大表再抄一遍,Fastlane 里 lane 怎么写同样略过。我们就聊一件事——CI 为啥慢、self-hosted 怎么真能提速。签名和传 TestFlight 的细节,直接看 Fastlane 那篇 就行。

一句话决策

  1. 若日志里每次 DerivedData 都是空的——先上 self-hosted runner,再谈换芯片。
  2. queued 经常 >5 分钟——固定席位比买更多托管分钟更有效。
  3. 48h 日租跑通 workflow 再月租;节点靠近 Git 与 ASC,而非只靠近开发者家。

9)常见疑问

GitHub Actions iOS CI 慢,是 Mac 性能不够?

多半不是。你本地 M4 编得飞快,CI 却慢,通常是在排队,或者每次 job 都是冷启动——Pods 重装、DerivedData 空的,等于从零再来一遍。换 macos-latest 这类标签,机器还是用完就扔的 ephemeral 环境,体感往往好不了多少。

self-hosted runner 安全吗?

自家私有仓库、团队成员固定,风险可控。GitHub 明确说过:别让 fork 来的 PR 自动跑 self-hosted job,陌生人代码别在你机器上执行。密钥放 GitHub Secrets,机器 SSH 定期换钥,和管一台生产机差不多。

能和托管 Runner 混着用吗?

当然可以,很多人就这么干:PR 检查扔 Linux,省 macOS 分钟;真要 Archive 再上 runs-on: [self-hosted, macos]。偶尔跑一次的杂活继续用托管也行,别让一台 Mac mini 扛所有 job。

Flutter 项目也适用吗?

适用。flutter build ipa 一样吃 DerivedData 和 Pods,道理没变。具体怎么配 workflow,看 Flutter iOS CI 那篇,Runner 架构跟这篇是一套。

非得买 Mac mini 吗?

不一定。你要的是一台只给你用的 macOS、磁盘能留着、别动不动就离线——自购放办公室、或者租云端裸金属都行。云端的好处是能按日试、换地区方便,不用自己折腾机房。

托管 macOS 分钟用得不多,还要 self-hosted 吗?

每月就几百分钟、偶尔编一次、排队也能忍,托管够用。要是 main 日更、好几条 release 线、或者经常全量编译,一般一两个月就会觉得「怎么又等了二十分钟」——那时候再考虑固定席位不迟。

macOS / Xcode 升级谁管?

self-hosted 得你自己管(或看云厂商镜像策略)。建议在 Runner 上把 Xcode 路径钉死(xcode-select),升级前先开分支跑一条试编译,别在大半夜 main 构建突然挂掉才发现 Xcode 变了。

28 分钟压到 9 分钟,能保证吗?

不能保证,文中数字是多家客户的样本中位数,不是承诺。小项目、或者 GitHub 缓存已经命中得很好,差距会小很多;超大 monorepo、每次 pod install --repo-update,该慢还是慢。拿自己仓库按上文 48 小时清单跑一遍,比看别人的表靠谱。

说白了:CI 慢到忍不了的时候,先想有没有固定席位、磁盘能不能留缓存,别急着再买一摞托管分钟。找台真 Mac 注册个 self-hosted runner,让 DerivedData 撑过第二个 job——差距通常一眼就能看出来。

限时优惠