云端 Mac mini 上的 Flutter 双端出包:fvm 版本锁定与 Gradle 缓存

CI/CD 实践 ·约 6 分钟阅读

云端 Mac mini 上的 Flutter 双端出包:fvm 版本锁定与 Gradle 缓存

团队群里飘来一句吐槽:「Android 出包在 CI 里三分钟,iOS 出包本地跑了十五分钟,还得手动切 Xcode 版本。」这是很多 Flutter 团队的日常——一份代码,两条构建路径,却各自维护一套环境。把双端出包挪到同一台云端 Mac mini 上,用 fvm 锁死 Flutter 版本、把 Gradle 与 CocoaPods 的缓存打理妥当,两条路径其实可以共享大部分预热成本。下面是一套跑得通的配置记录。

为什么把双端出包放在一台 Mac 上

Android 单独出包本可以放在便宜的 Linux 机器上,但 Flutter 项目的痛点是 iOS 和 Android 共用同一份 pubspec.yaml 依赖与同一个 Flutter SDK 版本。如果两端环境割裂,团队要维护两份版本对齐记录,升级一次 Flutter 就要在两台机器上分别验证。放在一台独享物理 Mac mini 上,~/fvm~/.pub-cache、Gradle 与 CocoaPods 的缓存目录都能被两端复用,省下的是重复下载与重复解析依赖的时间,而不是构建本身的 CPU 时间。

独享物理机的好处不止是性能,更是缓存的连续性——共享虚拟机上的缓存随时可能被邻居的构建挤掉,独享环境不会。

用 fvm 锁定版本,团队构建才能对齐

在项目根目录固定版本:

brew install fvm
fvm install 3.24.3
fvm use 3.24.3 --force
echo "3.24.3" > .fvmrc

之后所有构建命令都走 fvm flutter 而不是全局 flutter,避免团队成员本地版本漂移:

fvm flutter pub get
fvm flutter build ios --release --no-codesign
fvm flutter build appbundle --release

CI 脚本里加一段版本校验,防止有人手动改了全局 Flutter 却忘了同步 .fvmrc:

LOCKED=$(cat .fvmrc)
CURRENT=$(fvm flutter --version | head -1 | awk '{print $2}')
if [ "$LOCKED" != "$CURRENT" ]; then
  echo "flutter version mismatch: locked=$LOCKED current=$CURRENT" >&2
  exit 1
fi

Android 侧:SDK、NDK 与 Gradle 缓存

云端 Mac mini 默认不带 Android SDK,首次搭建按这个顺序走:

  1. brew install --cask android-commandlinetools
  2. sdkmanager --install "platforms;android-34" "build-tools;34.0.0" "ndk;26.3.11579264"
  3. ~/.gradle/gradle.properties 里打开并行与守护进程:
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096m

Gradle 守护进程默认在构建结束后闲置十分钟就退出,如果构建节奏密集,建议把 --max-workers 显式设为机器核数减一,避免和 iOS 侧的 xcodebuild 抢 CPU。

首次 flutter build appbundle 会拉取全部 Gradle 依赖,记录下这次耗时,后续对比缓存生效前后的差异更直观。

iOS 侧:证书、Pod 缓存与 flutter build ipa

CocoaPods 的缓存分两层,一层是 CDN 下的 pod spec 索引,一层是各 pod 源码本身:

pod cache list | head
export CP_HOME_DIR="$HOME/.cocoapods-cache"
cd ios && pod install --repo-update

证书与描述文件建议走 match 式的集中管理,而不是每次手动导入,配置好后出包只是一条命令:

fvm flutter build ipa --release \
  --export-options-plist=ios/ExportOptions.plist

签名报错九成集中在 Provisioning Profile 与 Bundle ID 不匹配,构建前用 security find-identity -v -p codesigning 确认签名身份是否还在钥匙串里有效,比出错后再排查快得多。

双端流水线与耗时对照

在一台 M4 云主机上,预热缓存前后的两次构建耗时差异明显:

阶段 首次(冷缓存) 后续(热缓存)
pub get 依赖解析 约 40 秒 约 8 秒
Android Gradle 构建 约 6 分钟 约 1 分 40 秒
iOS Pod install 约 2 分钟 约 20 秒
iOS xcodebuild 出包 约 5 分钟 约 3 分钟

热缓存的前提是 ~/.gradle~/.pub-cacheios/Pods~/fvm 四个目录都没被清空,团队协作时建议把这四个目录纳入机器级备份,而不是每个人各自缓存一份。

常见踩坑清单

  • NDK 版本与 Gradle 插件版本不匹配:ndk;26.3.11579264 要和 android/app/build.gradle 里声明的 ndkVersion 完全一致,否则构建时才报错,浪费一整轮时间。
  • CocoaPods 与 Xcode 版本联动:升级 Xcode 后如果没跑 pod deintegrate && pod install,偶发签名失败会被误判成证书问题。
  • fvm 缓存目录被误清理:清理磁盘空间时容易把 ~/fvm/versions 当垂悬文件删掉,建议单独加进排除列表。
  • Gradle 守护进程占内存不释放:长期跑构建的机器建议定期 ./gradlew --stop,释放内存给后续的 iOS 构建。

常见问题

云端 Mac mini 一定要用来出 Android 包吗?本地 Linux 机器不行吗?

Android 单独出包确实不必上 Mac,但 Flutter 项目通常 iOS 与 Android 共用一套代码与依赖版本,放在同一台 Mac mini 上可以让 fvm 锁定的 Flutter 版本、pub 缓存和 Gradle 缓存被两端复用,省掉维护两套构建环境的成本,尤其适合小团队按天或按周租用来集中出双端包。

fvm 锁定版本后,团队新人换机器还要重新下载一次 Flutter SDK 吗?

第一次是的,fvm 会按项目 .fvmrc 里指定的版本下载对应 SDK 到 ~/fvm/versions,之后同版本的项目会直接复用缓存,不会重复下载;在云端 Mac mini 上把 ~/fvm 目录纳入定期镜像或快照,团队多人共用同一台机器时几乎零等待。

Gradle 和 CocoaPods 的缓存放在云端主机上,租期到期数据会不会丢?

云端 Mac mini 独享物理机在租期内数据完全由你掌控,不会被清空,但到期前建议把 ~/.gradle、~/.pub-cache 与 Pods 目录一并打包备份,续租直接恢复即可继续复用缓存,避免下一次租用从零重建。

控制台://order

现在就要一台?

独享物理 Mac mini,按天起租,约 4 分钟从付款到可用。

开机 →