项目仓库快 40GB,Pods、node_modules、DerivedData 加起来比源码本身还大。你在本地打开 Xcode,项目挂载在远端云端 Mac 的一个 SMB 共享盘上,结果索引转了一分多钟,切个分支卡到怀疑网络掉线——这是很多把开发环境搬到云端 Mac 之后第一个撞上的墙。挂载共享盘看起来省事,实际用起来才发现,大型 iOS 工程的文件访问模式跟网络文件系统天生不合拍。这篇文章记录我们在 HireVPS 节点上用 Mutagen 做本地与远程双向同步,把这套体验拉回接近本地的过程。
挂载共享盘为什么不适合大型 iOS 工程
NFS、SMB 这类网络文件系统的设计前提是「偶尔访问的大文件」,而 Xcode 索引、SourceKit、Pods 的 CocoaPods 缓存扫描,做的恰恰是相反的事:短时间内对几万个小文件做 stat、open、close。每一次操作都要走一趟网络往返,哪怕延迟只有 20ms,乘以几万次也是分钟级的卡顿。更麻烦的是断网处理——挂载点在连接抖动时会直接假死,终端和编辑器一起卡死,只能强制卸载重新挂载。
三种常见方案对比一下会更直观:
| 方案 | 实时性 | 大量小文件表现 | 断线恢复 | 适用场景 |
|---|---|---|---|---|
| SMB/NFS 挂载 | 强(所见即最新) | 差,逐文件网络往返 | 差,容易假死 | 偶尔读写大文件 |
| rsync 定时任务 | 弱,有同步窗口 | 一般,批量传输效率高 | 好,任务制重跑即可 | 定期备份、单向推送 |
| Mutagen 双向同步 | 接近实时 | 好,本地缓存+增量传输 | 好,断线自动重连续传 | 持续开发的双向协作 |
我们踩过的一个教训是:一开始图省事直接挂载,结果 Xcode 的「Building workspace」这一步平均要多等 15~25 秒,团队每天光是等待索引就浪费不少时间,换成 Mutagen 之后这个等待基本消失。
用 Mutagen 建立双向同步会话
安装与首次连接
Mutagen 本身是单一二进制,本地和云端 Mac 都不需要额外常驻服务,连接建立时会自动把 agent 分发到远端。本地先安装:
brew install mutagen-io/mutagen/mutagen
mutagen version
假设你的云端 Mac 已经开好 SSH,凭据在开机邮件里。创建一个同步会话:
mutagen sync create \
--name=ios-app \
--ignore-vcs \
--symlink-mode=posix-raw \
/Users/me/Projects/ios-app \
ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app
--ignore-vcs 会自动跳过 .git 内部对象(git 本身用自己的协议同步更合适,不需要 Mutagen 重复搬),--symlink-mode=posix-raw 让 CocoaPods 里常见的符号链接原样保留,不做转换。
忽略规则:哪些目录绝对不能同步
工程目录下一定要建一份 .mutagenignore,否则第一次同步就会把几个 GB 的缓存目录也传一遍:
DerivedData/
Pods/
.build/
node_modules/
*.xcuserstate
xcuserdata/
.DS_Store
这些目录的共同点是「可以在任意一端本地重新生成」,同步过去不但浪费流量,还可能带来下面这节说的坑。
Xcode 相关的几个坑
- DerivedData 绝对不能同步:里面的模块缓存(ModuleCache.noindex)和构建索引跟本机的绝对路径、架构绑死,同步到另一台机器基本等于强制触发全量重新索引,严重时还会报出莫名其妙的编译错误。让本地和云端各自独立生成即可,反正它本来就该是可丢弃的产物。
- Pods 与 node_modules 各自安装:源码里的
Podfile.lock、package-lock.json才是需要同步的东西,依赖目录本身在两端分别跑一次pod install、npm install,既省流量也避免二进制产物跨架构不兼容。 .gitignore与.mutagenignore保持一致:两份忽略清单如果打架,你会看到 git 显示「无改动」,但 Mutagen 却在后台传一堆没意义的文件,排查起来很浪费时间,建议写个小脚本每次改动后 diff 一下两份清单。- 权限位差异:云端 Mac 与本地账户的 UID 不同,遇到权限报错先看是不是同步把可执行位丢了,
--permissions-mode=portable能缓解大部分场景。
实测表现与冲突处理
日常使用下,一次保存触发的增量同步(改动几个 Swift 文件)基本在一两秒内完成,肉眼感觉跟本地保存差不多;第一次全量同步(几万个源文件)因为要扫描目录树,会明显慢一些,建议开工前先跑一次让它稳定,而不是边写代码边等首次同步。
冲突处理上,默认的双向模式是 two-way-safe——遇到冲突不会自动覆盖,而是停下来等你处理,这一点比强制覆盖的方案安全,但如果两边都在改同一个文件就会频繁暂停。团队协作场景更推荐:
mutagen sync create \
--name=ios-app \
--sync-mode=two-way-resolved \
--default-file-mode-alpha=0644 \
/Users/me/Projects/ios-app \
ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app
two-way-resolved 会在冲突时默认让本地(alpha 端)胜出,配合「一个人同一时间只在一端改代码」的约定,基本不会丢改动。真遇到二进制资源(图片、字体)冲突,mutagen sync list 会列出冲突路径,手动比对时间戳和文件大小决定留哪份。
一份可以直接抄的配置
Mutagen 支持项目级配置文件,放在工程根目录的 mutagen.yml 里,团队成员克隆仓库后一行命令就能起同步,不用每个人手敲一遍参数:
sync:
ios-app:
alpha: "."
beta: "ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app"
mode: "two-way-resolved"
ignore:
vcs: true
paths:
- "DerivedData"
- "Pods"
- "node_modules"
- "xcuserdata"
- ".build"
symlink:
mode: "posix-raw"
常用的日常命令记这几个就够:
mutagen sync list
mutagen sync monitor ios-app
mutagen sync pause ios-app
mutagen sync resume ios-app
mutagen sync terminate ios-app
monitor 在排查「怎么改动没同步过去」时特别好用,能实时看到扫描、暂存、传输三个阶段各卡在哪一步。
上线前自查清单
.mutagenignore和.gitignore覆盖 DerivedData、Pods、node_modules、xcuserdata 等可重建目录了吗?- 同步模式选的是
two-way-safe还是two-way-resolved?团队协作场景优先后者并约定好谁的改动优先。 - 首次全量同步是不是在没有紧急任务的时间段做的?几万文件的初始扫描不适合边写代码边等。
- 云端 Mac 的具体机型与节点是否已在控制台确认当前可选配置?带宽和磁盘规格会影响同步速度上限。
mutagen sync monitor是不是已经养成排查习惯,而不是等文件丢了才想起来看日志?
把这几项过一遍,云端 Mac 的编辑体验基本就能贴近本地,团队协作时也不会再因为「谁的改动没同步过去」互相甩锅。
常见问题
为什么不直接用 SMB/NFS 把云端 Mac 目录挂载到本地?
网络挂载对大量小文件(如 Xcode 索引、Pods 目录)的每次 stat/open 都要走一次网络往返,大型 iOS 工程打开项目或索引常常卡到几十秒;Mutagen 走本地文件系统缓存加增量同步,交互延迟更低,断网也不会导致文件系统假死。
DerivedData 要不要同步?
不要。DerivedData 里的模块缓存、索引文件跟机器架构和本机路径绑定,同步过去大概率触发重新索引甚至编译报错,应该把它加进 .mutagenignore,让本地和云端各自独立生成。
双向同步出现冲突怎么处理?
先用 mutagen sync list 定位冲突文件,大多数场景选 two-way-resolved 模式并设置 alpha 端(本地)优先,极少数二进制资源冲突再手动 diff 决定保留哪一份,养成同步前 git status 清一次的习惯能避免大部分冲突。
HireVPS
今天就试试独享云 Mac mini
按天起租,2 分钟拿到 SSH/VNC 凭据,配置随时可升级。