凌晨两点,CI 流水线发出告警:构建节点磁盘剩余不足 2 GB,xcodebuild 直接退出。你打开终端,df -h 一看,256 GB 的 SSD 被塞满了——~/Library/Developer 目录独占 180 GB,其中一个叫 DerivedData 的目录就占了 90 GB 以上。
这不是偶发事故。在一台跑了 8 个月、未做任何清理的 M4 Mac Mini 上,DerivedData 平均能积累 40–100 GB;加上模拟器镜像、设备符号表,~/Library/Developer 合计超过 150 GB 并不罕见。本文从根因分析→快速定位→分类处理→CI 防阻塞→自动化维护五个环节,给你一套可以直接照抄的操作手册。
读完这篇文章,你会得到:DerivedData 各子目录的实际用途与安全性评估、一条命令定位最大磁盘占用源、CI/本地双场景的清理脚本,以及一套让磁盘告警永不再触发的自动化方案。
Quick Answer:三个最常见问题
| 问题 | 直接答案 | 风险 |
|---|---|---|
| DerivedData 能直接删吗? | rm -rf ~/Library/Developer/Xcode/DerivedData/* 完全安全 |
下次构建变全量,耗时增加约 2–5 分钟 |
| Archives 能删吗? | 已无线上用户的版本可删;现役版本保留 dSYM | 删后无法符号化旧版崩溃日志 |
| CI 节点磁盘快满了怎么快速救急? | DerivedData → 模拟器 unavailable → 旧版 DeviceSupport,依次清理 | 下次构建需重建索引,约慢 3 分钟 |
DerivedData 究竟存了什么
很多开发者只知道「清 DerivedData 能解玄学问题」,但不清楚里面到底放了什么。这会导致一个后果:要么删了不该删的,要么留着该删的、磁盘一次次撑爆。下表列出 ~/Library/Developer 下最重要的几个路径:
| 路径 | 内容 | 典型大小 | 安全删除? |
|---|---|---|---|
Xcode/DerivedData |
编译产物、索引、构建日志、Swift 模块缓存 | 5–60 GB | ✅ 完全安全,下次构建自动重建 |
Xcode/Archives |
发布构建(.xcarchive)含 dSYM 符号表 | 2–30 GB | ⚠️ 旧版可删,现役版本保留 dSYM |
Xcode/iOS DeviceSupport |
连接实机时下载的调试符号 | 1–3 GB / 每 iOS 版本 | ✅ 旧版可删,重连设备自动重下 |
CoreSimulator/Profiles/Runtimes |
模拟器运行时镜像 | 5–10 GB / 每 iOS 版本 | ⚠️ 不用的版本可删,需重下 |
CoreSimulator/Devices |
已创建的模拟器实例数据 | 1–15 GB | ✅ unavailable 状态可安全删 |
Caches/org.swift.swiftpm |
SPM 包缓存 | 1–8 GB | ✅ 可删,下次 resolve 自动重建 |
Caches/com.apple.dt.Xcode |
Xcode 内部缓存(预览、IB) | 0.5–3 GB | ✅ 可删 |
DerivedData 子目录结构
每个项目在 DerivedData 下有一个独立子目录,格式为 {项目名}-{随机哈希}/,内部结构如下:
MyApp-abcdefghijklmn/
├── Build/
│ ├── Products/ # 编译产物(.app, .framework, .o 文件)
│ └── Intermediates.noindex/ # 中间文件(.o, .d 依赖图)
├── Index.noindex/ # SourceKit 索引(代码跳转、自动补全)
├── Logs/ # 构建日志(xcodebuild -resultBundlePath 也在这里)
└── info.plist # 项目路径记录
其中 Build/Intermediates.noindex 通常最大——一个含 200 个 Swift 文件的中型项目,这个目录轻松超过 2 GB;加上 Index.noindex,单个项目的 DerivedData 目录达到 5–10 GB 并不罕见。
五步快速定位磁盘阻塞元凶
不要一上来就无差别清理。花 3 分钟定位,比乱删省时省力。下面这套流程在一台 256 GB M4 Mac Mini 上实测,可在 2 分钟内找到占用 Top 5 的目录。
Step 1:先看磁盘整体使用情况
# 查看全盘使用率
df -h /
# 快速定位前 10 大目录(整个根盘)
sudo du -sh /* 2>/dev/null | sort -rh | head -10
Step 2:锁定 Developer 目录的具体分布
du -sh ~/Library/Developer/Xcode/* 2>/dev/null | sort -rh
du -sh ~/Library/Developer/CoreSimulator/Profiles/Runtimes/* 2>/dev/null | sort -rh
du -sh ~/Library/Developer/Xcode/iOS\ DeviceSupport/* 2>/dev/null | sort -rh
实测输出示例(跑了 8 个月的 CI 节点):
91G /Users/ci/Library/Developer/Xcode/DerivedData
28G /Users/ci/Library/Developer/CoreSimulator
18G /Users/ci/Library/Developer/Xcode/iOS DeviceSupport
12G /Users/ci/Library/Developer/Xcode/Archives
6G /Users/ci/Library/Developer/Xcode/Caches
Step 3:找出 DerivedData 里最胖的子项目
du -sh ~/Library/Developer/Xcode/DerivedData/* 2>/dev/null | sort -rh | head -10
Step 4:查看模拟器占用与状态
# 列出所有模拟器及状态
xcrun simctl list devices
# 只看"已不可用"(系统升级后遗留)
xcrun simctl list devices | grep -i "unavailable"
Step 5:确认 DeviceSupport 的版本分布
ls -lh ~/Library/Developer/Xcode/iOS\ DeviceSupport/
输出如下时,说明积累了多年的设备符号文件:
drwxr-xr-x 3 dev staff 96B iOS 15.4 (19E241)
drwxr-xr-x 3 dev staff 96B iOS 16.1 (20B82)
drwxr-xr-x 3 dev staff 96B iOS 17.2 (21C62)
drwxr-xr-x 3 dev staff 96B iOS 18.3.2 (22D82)
iOS 15、16 的符号表在 2026 年几乎没有保留价值(App Store 最低版本要求已普遍达到 iOS 17),可以放心删掉。
各目录处理策略:能删、慎删、不能删
可以直接删(零风险)
# 1. 清空所有项目的编译缓存
rm -rf ~/Library/Developer/Xcode/DerivedData/*
# 2. 删除已不可用的模拟器
xcrun simctl delete unavailable
# 3. 清理 SPM 包缓存
rm -rf ~/Library/Caches/org.swift.swiftpm/*
# 4. 清理 Xcode 内部缓存
rm -rf ~/Library/Caches/com.apple.dt.Xcode/*
# 5. 清理 SwiftUI 预览缓存
rm -rf ~/Library/Developer/Xcode/UserData/Previews/*
选择性删除(删旧留新)
# 删除 2 个版本以前的设备符号(保留最新两个 iOS 版本)
# 先确认当前有哪些版本
ls ~/Library/Developer/Xcode/iOS\ DeviceSupport/
# 示例:只保留 iOS 18.x,删除更早的
rm -rf ~/Library/Developer/Xcode/iOS\ DeviceSupport/iOS\ 15*
rm -rf ~/Library/Developer/Xcode/iOS\ DeviceSupport/iOS\ 16*
rm -rf ~/Library/Developer/Xcode/iOS\ DeviceSupport/iOS\ 17*
需要谨慎的:Archives
- 确认该版本已从 App Store 下架或无活跃用户
- 若仍有用户:单独导出 dSYM(Organizer → 右键 Archive → Show in Finder → 取出 .dSYM)
- 将 dSYM 上传至 Firebase Crashlytics / Sentry 等崩溃分析平台
- 确认平台已收到 dSYM 后,再删除对应 Archive
一键救急命令(磁盘告急时)
以下命令按安全顺序执行,通常可快速释放 20–60 GB:
#!/bin/bash
# emergency-clean-xcode.sh
# 执行前确认:已无正在运行的 xcodebuild 进程
echo "=== Xcode 磁盘紧急清理 ==="
echo "[1/4] 清理 DerivedData..."
du -sh ~/Library/Developer/Xcode/DerivedData 2>/dev/null
rm -rf ~/Library/Developer/Xcode/DerivedData/*
echo "✓ DerivedData 已清空"
echo "[2/4] 删除不可用模拟器..."
xcrun simctl delete unavailable
echo "✓ 不可用模拟器已删除"
echo "[3/4] 清理 SPM 和 Xcode 缓存..."
rm -rf ~/Library/Caches/org.swift.swiftpm/*
rm -rf ~/Library/Caches/com.apple.dt.Xcode/*
echo "✓ 缓存已清理"
echo "[4/4] 清理预览缓存..."
rm -rf ~/Library/Developer/Xcode/UserData/Previews/*
echo "✓ 预览缓存已清理"
echo ""
echo "=== 清理完成,当前磁盘状态 ==="
df -h /
CI/CD 流水线防阻塞方案
本地开发机手动清理还好应付,但 CI Runner 长期无人值守,更需要系统性防护。这里以常见的自托管 macOS Runner(GitHub Actions / GitLab CI)为例,给出三套方案。
方案 A:固定 DerivedData 路径 + 增量缓存
把 DerivedData 指向可控路径,配合 CI 缓存机制实现跨 Job 复用:
# .github/workflows/ios-build.yml 片段
- name: Build
run: |
xcodebuild \
-project MyApp.xcodeproj \
-scheme MyApp \
-configuration Release \
-derivedDataPath /tmp/derived-data \
-resultBundlePath /tmp/result.xcresult \
build
- name: Cache DerivedData
uses: actions/cache@v4
with:
path: /tmp/derived-data
key: derived-data-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }}-${{ hashFiles('**/*.xcodeproj/project.pbxproj') }}
restore-keys: |
derived-data-${{ runner.os }}-
Package.resolved 和 project.pbxproj 的哈希,否则 Swift Package 升级后会命中旧缓存导致构建失败。
方案 B:构建前后自动清理老旧缓存
# 在构建步骤之前:清理超过 7 天未使用的 DerivedData 子目录
- name: Clean stale DerivedData
run: |
find ~/Library/Developer/Xcode/DerivedData \
-maxdepth 1 -mindepth 1 \
-type d \
-atime +7 \
-exec rm -rf {} +
echo "DerivedData 清理完成,当前大小:"
du -sh ~/Library/Developer/Xcode/DerivedData 2>/dev/null || echo "目录为空"
方案 C:GitLab CI 磁盘容量门控
在 .gitlab-ci.yml 里加一个前置检查 Job,磁盘不足时直接 fail-fast,避免构建到一半因磁盘满而崩溃,产生无效产物:
# .gitlab-ci.yml
check_disk:
stage: .pre
script:
- |
AVAIL=$(df -k / | awk 'NR==2 {print $4}')
THRESHOLD=$((10 * 1024 * 1024)) # 10 GB in KB
if [ "$AVAIL" -lt "$THRESHOLD" ]; then
echo "ERROR: 磁盘剩余不足 10 GB(当前: $((AVAIL/1024/1024)) GB),请先清理"
exit 1
fi
echo "磁盘检查通过,剩余: $((AVAIL/1024/1024)) GB"
tags:
- macos-runner
三套方案对比
| 方案 | 适用场景 | 实现成本 | 缓存复用率 |
|---|---|---|---|
| A:固定路径 + 增量缓存 | 中大型项目,PR 频繁,构建耗时敏感 | 中(需配置 cache action) | 高(跨 Job 复用) |
| B:定期清理老旧子目录 | 多项目共用节点,磁盘中等充裕 | 低(一行 find 命令) | 中(当日复用) |
| C:容量门控 fail-fast | 任何场景,作为兜底保障 | 极低(5 行脚本) | 不影响复用 |
推荐三套方案叠加使用:A 提升构建速度,B 控制磁盘增长,C 作为最后防线。
自动化维护:定时脚本 + 容量预警
手动清理终究不可靠。把清理逻辑写成脚本,挂到 macOS launchd 定时执行,磁盘问题从被动应急变为主动防御。
完整维护脚本
#!/bin/bash
# xcode-maintenance.sh
# 建议每周日凌晨 3 点执行
# 放置路径:~/scripts/xcode-maintenance.sh
LOGFILE=~/Library/Logs/xcode-maintenance.log
THRESHOLD_GB=20 # 当剩余磁盘低于此值时触发深度清理
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE"; }
log "=== Xcode 定期维护开始 ==="
# 1. 记录清理前的磁盘状态
BEFORE=$(df -h / | awk 'NR==2 {print $4}')
log "清理前磁盘可用: $BEFORE"
# 2. 删除 14 天未访问的 DerivedData 子目录
log "清理 14 天以上未使用的 DerivedData..."
find ~/Library/Developer/Xcode/DerivedData \
-maxdepth 1 -mindepth 1 -type d -atime +14 \
-exec rm -rf {} + 2>/dev/null
log "✓ DerivedData 老旧条目已清理"
# 3. 删除不可用模拟器
log "清理不可用模拟器..."
xcrun simctl delete unavailable 2>/dev/null
log "✓ 不可用模拟器已删除"
# 4. 清理 SPM 缓存(超过 30 天)
find ~/Library/Caches/org.swift.swiftpm -maxdepth 2 \
-type f -atime +30 -delete 2>/dev/null
log "✓ 老旧 SPM 缓存已清理"
# 5. 检查剩余空间,若低于阈值触发深度清理
AVAIL_GB=$(df -g / | awk 'NR==2 {print $4}')
if [ "$AVAIL_GB" -lt "$THRESHOLD_GB" ]; then
log "⚠️ 磁盘剩余 ${AVAIL_GB}GB < ${THRESHOLD_GB}GB,触发深度清理"
rm -rf ~/Library/Developer/Xcode/DerivedData/*
rm -rf ~/Library/Caches/com.apple.dt.Xcode/*
rm -rf ~/Library/Developer/Xcode/UserData/Previews/*
log "✓ 深度清理完成"
fi
AFTER=$(df -h / | awk 'NR==2 {print $4}')
log "清理后磁盘可用: $AFTER"
log "=== 维护完成 ==="
注册为 launchd 定时任务
将脚本注册为每周日凌晨 3 点自动执行:
# 1. 给脚本加执行权限
chmod +x ~/scripts/xcode-maintenance.sh
# 2. 创建 launchd plist
cat > ~/Library/LaunchAgents/com.myteam.xcode-maintenance.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.myteam.xcode-maintenance</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/YOUR_USER/scripts/xcode-maintenance.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key><integer>0</integer>
<key>Hour</key><integer>3</integer>
<key>Minute</key><integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/tmp/xcode-maintenance-stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/xcode-maintenance-stderr.log</string>
</dict>
</plist>
EOF
# 3. 加载
launchctl load ~/Library/LaunchAgents/com.myteam.xcode-maintenance.plist
echo "定时任务已注册"
云 Mac 如何从根本上规避磁盘困境
以上所有方案都是在「有限磁盘」的前提下做缓解。如果你的团队正在评估是否升级 CI 基础设施,有一个选项值得认真考虑:用云端独享 Mac Mini 替代本地节点。
磁盘管理问题在云端架构下有几个不同的解决路径:
- 加购扩展 SSD:Macstripe 的 M4 Mac Mini 支持扩展存储,直接买更大的盘比手动清理省力得多
- 周期性重置节点:按月计费的节点在周期结束后可以重新开通一台干净的机器,DerivedData 从零开始,无需任何清理脚本
- 按需扩容:版本发布冲刺期增加节点数量分担构建负载,低峰期缩减,无需为高峰容量长期付费
- SSH 直连调试:当节点出现玄学构建问题时,
ssh runner@your-node直接登机排查,比远程管理实体机方便
一个实际案例:某移动端团队将 3 台本地 Mac Mini 替换为 Macstripe 云节点后,每月节省的运维时间(清理磁盘、更新 Xcode、排查 Runner 故障)约 8–12 小时。按工程师时薪计算,光运维成本就覆盖了租赁费用的大部分。
对于已有 GitHub Actions 工作流的团队,也可以参考这篇关于 实测 Windows 能独立开发 iOS!别再买 Mac? 的文章,了解在不同平台上构建 iOS 应用时的完整对比。
FAQ
Q:直接删除 DerivedData 会不会影响源码或发版记录?
不会。DerivedData 只存放编译产物、SourceKit 索引和构建日志,与你的源码、Git 记录、Provisioning Profile、App Store Connect 提交完全独立。删除后 Xcode 会在下次构建时完整重建,唯一的代价是全量构建而非增量构建,中型项目约多花 2–5 分钟。
Q:Archives 目录可以随便删吗?
不能随便删。Archives 里的 .xcarchive 包含 dSYM 符号表,用于符号化线上崩溃日志。删除前请确认:① 该版本已从 App Store 下架且无活跃用户;② 或已将 dSYM 上传至 Firebase Crashlytics / Sentry 等平台。建议保留最近 3 个发布版本的 Archive,更早的可在导出 dSYM 后删除。
Q:CI 环境每次构建都全量重建,有办法复用缓存吗?
可以。把 -derivedDataPath /tmp/derived-data 传给 xcodebuild,再结合 GitHub Actions actions/cache 或 GitLab CI 的 cache: 配置做增量同步。Cache key 需要包含 Package.resolved 和 project.pbxproj 的哈希,避免不同依赖版本互相污染。实测对 Swift Package 较多的项目,增量构建可节省 40–60% 时间。
Q:磁盘空间不足导致 xcodebuild 直接失败,如何快速恢复?
按优先级依次执行:① rm -rf ~/Library/Developer/Xcode/DerivedData/*(通常释放 10–60 GB);② xcrun simctl delete unavailable(释放 5–20 GB);③ 删除两个版本以上的 iOS DeviceSupport 条目(释放 5–15 GB)。大多数情况下第①步就能解决问题。
Q:Xcode 的「Product > Clean Build Folder」和命令行删除 DerivedData 有什么区别?
Clean Build Folder 只清理当前打开项目的构建产物,对其他项目的缓存、模拟器数据、DeviceSupport 没有影响。命令行 rm -rf ~/Library/Developer/Xcode/DerivedData/* 会清掉所有项目的缓存。CI 节点维护时建议用命令行,开发机日常调试用 Clean Build Folder 更精准。
Q:在 CI Runner 上,DerivedData 增长特别快怎么处理?
原因通常是:多个 Scheme / 多个分支并发构建,每次都产生新的 DerivedData 子目录。建议:① 用 -derivedDataPath 固定到单一路径;② 在构建脚本里加 find ... -atime +7 -exec rm -rf {} + 清理 7 天未访问的子目录;③ 评估是否需要升级节点磁盘容量或使用 Macstripe 云 Mac 的扩展 SSD 选项。
总结
DerivedData 磁盘阻塞是 iOS 开发者几乎必然会遇到的问题,但它完全可以通过系统性手段解决,而不是每次「玄学」发作时手忙脚乱地清理。
- 理解根因:DerivedData 是纯缓存,可安全删除;Archives 含 dSYM,需谨慎处理
- 快速诊断:
du -sh ~/Library/Developer/Xcode/* | sort -rh一条命令定位元凶 - 分类处理:DerivedData 无脑删,DeviceSupport 删旧留新,Archives 先导出 dSYM 再删
- CI 防护:固定
-derivedDataPath+ 增量缓存 + 容量门控三层保障 - 自动化:用 launchd 挂载维护脚本,每周自动清理,从被动应急转为主动防御
- 从根本解决:评估云 Mac 方案,通过扩展 SSD 或周期性重置节点彻底告别磁盘焦虑
如果你的团队正面临 CI 节点磁盘频繁告警、构建中途失败的困扰,不妨先跑一次文中的诊断脚本,通常 10 分钟内就能释放 30 GB 以上。长期来看,将 CI 基础设施迁移到云端独享 Mac Mini 是更可持续的解法——既能按需扩容,又无需担心磁盘维护。