团队群里飘来一句吐槽:「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,首次搭建按这个顺序走:
brew install --cask android-commandlinetoolssdkmanager --install "platforms;android-34" "build-tools;34.0.0" "ndk;26.3.11579264"- 在
~/.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-cache、ios/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 分钟从付款到可用。