Mac 开发者在终端排查 Xcode DerivedData 磁盘占用问题

凌晨两点,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 下的所有内容均为可重建缓存,与源码、Provisioning Profile、App Store 提交记录完全独立。删除后的唯一代价是下次构建变全量(中型项目约多 2–5 分钟)。

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
实测数字:在一个有 15 个历史项目的开发机上,top 3 的 DerivedData 子目录分别为 18 GB、12 GB、9 GB——其中两个项目已经超过一年没打开过,白占了 30 GB。

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 节点是本地实体机,磁盘管理终究是个长期负担。Macstripe 提供云上独享 M4 Mac Mini,可按天/周/月计费,用完即可重置节点——DerivedData 堆积问题从根本上消失。查看定价方案 →

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 }}-
注意:缓存 key 必须包含 Package.resolvedproject.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 "定时任务已注册"
实测效果:某团队在 4 台自托管 Mac Mini CI 节点上部署此脚本后,连续 3 个月未再出现磁盘告警。每周定期清理维持在 3–5 GB 的回收量,节点稳定保持 60 GB 以上可用空间。

云 Mac 如何从根本上规避磁盘困境

以上所有方案都是在「有限磁盘」的前提下做缓解。如果你的团队正在评估是否升级 CI 基础设施,有一个选项值得认真考虑:用云端独享 Mac Mini 替代本地节点

磁盘管理问题在云端架构下有几个不同的解决路径:

  • 加购扩展 SSD:Macstripe 的 M4 Mac Mini 支持扩展存储,直接买更大的盘比手动清理省力得多
  • 周期性重置节点:按月计费的节点在周期结束后可以重新开通一台干净的机器,DerivedData 从零开始,无需任何清理脚本
  • 按需扩容:版本发布冲刺期增加节点数量分担构建负载,低峰期缩减,无需为高峰容量长期付费
  • SSH 直连调试:当节点出现玄学构建问题时,ssh runner@your-node 直接登机排查,比远程管理实体机方便

一个实际案例:某移动端团队将 3 台本地 Mac Mini 替换为 Macstripe 云节点后,每月节省的运维时间(清理磁盘、更新 Xcode、排查 Runner 故障)约 8–12 小时。按工程师时薪计算,光运维成本就覆盖了租赁费用的大部分。

Macstripe 云 Mac:独享物理 M4 Mac Mini,全球五节点(新加坡 / 日本 / 韩国 / 香港 / 美国西部),SSH + VNC 双接入,约 5 分钟开通,无长约可按天计费。查看套餐与起价 →

对于已有 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.resolvedproject.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 是更可持续的解法——既能按需扩容,又无需担心磁盘维护。

相关阅读