- 问题: 我们的检测器把 16.9 GB 的 iOS 模拟器镜像判成了已废弃。macOS 不让任何人删它们,Apple 自己的工具试着处理,反而留下了 34 GB 失败的副本。
- 我们发现: 它们根本没废弃,是两个已安装模拟器的底层镜像,只是登记表丢了记录。删掉那两个运行时只拿回 21 MB,能删镜像的只有 Xcode 的 Components 面板。
- 我们做了什么: Mint 要删掉所谓的孤儿之前,会先让登记表重新认领一遍;这些镜像放在「保留」里,注明去 Xcode 删。它也不再走那条会留下 8 GB 副本的路。
我们上线过一个检测器,它在一台开发者的 Mac 上找出了 16.9 GB「已废弃的 iOS 模拟器运行时」,连版本号都报得出来。
结果它判断错了。那些文件其实是两个还装着的模拟器的底层镜像。
这篇写的是一个磁盘工具怎么一步步得出这种错误结论的。每一步都有坑,其中三个现在还在占着很多人的磁盘空间。下面的数字都是 2026 年 8 月 19 到 21 日在同一台 Mac 上实测的,你都可以自己复现。
macOS 不让你删的那堆垃圾
Xcode 每次下载 iOS 模拟器,系统镜像都会放进 Apple 的资产仓库:/System/Library/AssetsV2/com_apple_MobileAsset_iOSSimulatorRuntime/<hash>.asset,每个 8-9 GB。负责运行模拟器的服务叫 CoreSimulator,它有一份自己知道的运行时登记表,用 xcrun simctl runtime list 就能打出来。
那天早上,登记表里只有一个运行时(iOS 26.5),目录里却有三个。另外两个是 iOS 18.2 和 iOS 18.4,分别是 8.4 GB 和 8.5 GB,2025 年初下载的。
两个大镜像,登记表里却没有任何记录指向它们。典型的「孤儿」文件就长这样,我们的扫描器也是这么判断的。
第一个意外是,你根本删不掉它们:
$ ls -lO /System/Library/AssetsV2/com_apple_MobileAsset_iOSSimulatorRuntime/
drwxr-xr-x@ 5 _nsurlsessiond wheel restricted 160 Feb 4 2025 3ee34d77….asset
drwxr-xr-x@ 5 _nsurlsessiond wheel restricted 160 Apr 8 2025 6fb1e5fe….asset
restricted 是系统完整性保护(SIP)的标记,谁都动不了这个目录,sudo rm 也不行。网上最常见的建议是「关掉 SIP、重启、删除、再打开 SIP」,这种办法我们不会拿给用户用。
Apple 自己的运行时服务倒是能删:先用 simctl runtime add <镜像> --move 让它把镜像重新认领回登记表,再按 UUID 删除。删除由守护进程用它自己的权限来做,不需要密码。
一次被拒,代价 8 GB
认领被拒绝了:
$ xcrun simctl runtime add …/092-10337-196.dmg --move
SimDiskImageError code=14: Cannot copy the image because the disk is almost full
当时空余 24.7 GB,镜像 8.4 GB。守护进程的日志全被打了码,所以我们直接去翻它的二进制:
$ strings …/CoreSimulator.framework/…/simdiskimaged
minFreeSpace = %llu
Because source is on the same volume as inbox and clones are supported
will allow a lower free space limit
如果服务能对源文件做 APFS 克隆(写时复制,一瞬间完成,不多占一个字节),它几乎不需要空余空间。如果克隆不了,它就退回到逐字节完整拷贝,要求空余空间有「镜像大小 + 一大块预留」。
可它没法从受 SIP 保护的资产仓库里克隆。于是你不能删的那个目录,Apple 自己的工具也得先完整拷一遍,才肯帮你处理。
我们也反过来验证了一下:在 /tmp 放一个普通的 9 GB 文件,只剩 14.6 GB 空余,同样的预检照样通过。同一个卷,大小差不多,只是没有 SIP 保护,守护进程就没意见。
更糟的是,被拒绝的 runtime add 并非什么都没做。守护进程判定磁盘太满的时候,已经把镜像拷进了自己的仓库,还登记成了一个 Unusable 运行时,事后这两样都不会清掉。三次被拒之后,/Library/Developer/CoreSimulator/Cryptex/Images/ 里多了三份 8.38 GB 的拷贝,Inbox 里还有一份 9 GB。
加起来 34 GB 的残留,全是报了失败的操作留下的。没有工具会列出它:对主目录跑 du 看不到,它所属的那个运行时也不在 simctl runtime list 的普通输出里。
那些垃圾并不是垃圾
到这里,我们的结论是:这 16.9 GB 谁也删不掉,而那条认领的路每失败一次,情况就更糟一点。一条命令就推翻了这个结论。
xcrun simctl runtime scan-and-mount 是 Apple 自己的修复命令。它会让 CoreSimulator 把自己的存储过一遍,把丢了记录的东西重新认领回来,大概只要十分之一秒。跑完之后,登记表变成了这样:
$ xcrun simctl runtime list
iOS 18.2 (22C150) - 87410283-… (Ready)
iOS 26.5 (23F77) - 8E56E104-… (Ready)
iOS 18.4 (22E238) - 10B15C7F-… (Ready)
Total Disk Images: 3 (24.2G)
登记表里从一个运行时变成了三个。iOS 18.4 的完整 JSON 记录里还有一个字段,摘要输出里是看不到的:
parentImagePath = /System/Library/AssetsV2/…/6fb1e5fe….asset/AssetData/090-44150-318.dmg
原来那个「孤儿」asset 是一个已安装、能正常用的运行时的父镜像。CoreSimulator 把装着 iOS 18.2 和 18.4 的两个 Cryptex bundle 的登记记录弄丢了,记录缺失期间,机器上就没有任何东西认领这些 asset。文件本身一直没变,丢的只是登记表里的那条记录。
之后清理就很简单了。对那三个 Unusable 残留跑 simctl runtime delete <UUID>,34 GB 暂存残留全部清掉,不用密码,也不用暂存空间,Inbox 也顺手清空了。那两个 18.x 运行时现在在我们的扫描结果里显示正常:一个没有模拟器在用,一个已经被更新的版本取代,都能按 UUID 直接删。
然后我们又错了一次
可删掉它们几乎腾不出空间。我们用 Mint 跑了一遍,删除 iOS 18.2 运行时只拿回了 21 MB,离 8.4 GB 差得远。登记记录没了,bundle 也卸载了,但 8.4 GB 的 .asset 还在原地。simctl runtime delete 从来不删它当初安装时用的那个 MobileAsset。Apple 还专门提供了一个 --keep-asset 开关,看起来像是默认会删 asset,可在 Cryptex 支撑的运行时上,加不加这个开关都一样。
于是我们当时认为,要拿回这块空间只剩一个办法:用 runtime add --move 把镜像从受保护的仓库里迁出来。这条路没法克隆,需要空闲空间有「镜像大小 + 大约十分之一的磁盘容量」。一块 494 GB 的盘上有 16.9 GB 已经没用的模拟器,你就得先腾出大约 58 GB。
一周后的更新(我们腾出了那 58 GB,真的试了一次): 这条路也走不通。守护进程认领了自己那份副本,然后打出「--move was specified but unable to remove original file: Operation not permitted」。Apple 自己带特权的服务,和你一样删不掉封印资产库里的文件。每试一次,CoreSimulator 的 Images/Inbox 里还会多一份 8 GB 的暂存副本,我们试过的办法都删不掉它:simctl 不行,输入管理员密码用 root 跑 rm 也不行。放在 Apple 资产库里的运行时,没有任何程序能把那部分空间拿回来。现在还能删它们的只有 Xcode 的 设置 ▸ Components,已登记的运行时旁边有个移除按钮。
回头看,空间那笔账一直算得没错。我们先是把标签贴错了,后来又把路选错了。
测的过程中还冒出两个问题。删掉一个运行时再重新扫描,它又回来了:我们自己的修复步骤把留下的 asset 又登记了一遍,等于撤销了删除。另外,一次成功了的删除被报成了失败,因为我们检查时 CoreSimulator 还没更新登记表。这两个都修好了。直到开始测文件系统、不再只信登记表,我们才发现这两个 bug。
还有一个 Apple 的怪地方,自己写清理逻辑的人可以留意:simctl runtime delete --notUsedSinceDays 90 在这台机器上什么也没匹配到,可两个 18.x 运行时的 lastUsedAt 都是 null。Apple 这个「删掉很久没用的」过滤器,偏偏会跳过从来没用过的运行时。
一个目录,两个名字
做「磁盘空间都去哪了」那张图的时候,一个内部检查开始报警:Developer 分类下各子项加起来,比分类本身还多出 18.1 GB。这按理不可能,每个子项都是在父项里面测的。这个检查报了 502 次,每次多出来的都正好是 18.1 GB。
原因是 firmlink。从 Catalina 开始,macOS 把系统放在一个封印的只读卷上,把你的数据放在另一个卷上,再把两边拼起来,让文件系统看着像一棵树。所以有些路径有两种写法,指的是同一个目录:
$ stat -f '%d %i %N' /System/Library/AssetsV2 /System/Volumes/Data/System/Library/AssetsV2
16777233 210916113 /System/Library/AssetsV2
16777233 210916113 /System/Volumes/Data/System/Library/AssetsV2
设备号和 inode 都一样,只是名字不同。全盘测量用一种写法记下了模拟器目录,扫描器用另一种写法报了运行时,按字符串一比,以为是两个地方,于是 18.1 GB 又加到了本来就包含它的分类上。
磁盘工具都按路径去对两份测量结果,只要不经过 firmlink 表来比较,就会有这个 bug。这个问题在 APFS 之前就有:两个不一样的路径可能是同一个文件,所以要比设备号和 inode,不能只比字符串。
从来不存在的 46.7 GB
对 Chrome 的更新残留跑一下 du,它会报出 34 个 *.code_sign_clone 目录、共 46.7 GB。把它们全删掉,空余空间一点都不会变。
它们是 APFS 克隆。Chrome 在代码签名时会给自己的程序包做一个写时复制快照,克隆和原件共用所有数据块,除非其中一个被改动,而实际上从来不会改。du 报的是逻辑大小,所以一个 1.4 GB 的应用被算了 34 次。
按 du 排序的清理工具会把它们排在最前面,让你「释放」46.7 GB,删完一看,空余空间还是原来的数。我们测的是实际分配的数据块,总览里干脆不显示克隆目录。与其说能释放 46.7 GB 结果一点没释放,不如不提。
晚到一秒的删除
最后一个问题不大,就是我们写这篇文章那天碰上的。
当时我们的回执不报被删文件有多大,只报卷上测到的空余空间变化,在最后一个文件删掉后马上采样。用户能拿去跟访达对照的只有这个数,所以我们这么做,之前几个月也一直没出过问题。
后来有人删了两个 9 GB 的目录,回执显示释放了 0 字节。文件确实没了,历史记录里也写着永久删除 17.5 GB,但 375 毫秒后读到的卷空余空间一点没变。
APFS 回收大目录的数据块是异步的。几百 MB 基本马上完成,几十 GB 要好几秒,在这几秒里采样,什么变化都看不到。窗口顶部的大数字只读了一次,也没变,看上去就跟删除没生效一样。
修法很普通:Mint 现在会等卷的数字稳定下来再报告。另外,不管是哪个按钮触发的,只要释放了空间,之后都会重新读一遍顶部的数字。
我们改了什么
检测器我们留着,但它不能再凭一份没核实过的登记表就把东西判成孤儿。Mint 现在说某块模拟器存储是「孤儿」之前,会先让 CoreSimulator 重新认领一遍自己的存储,再重新读登记表。在这台 Mac 上这样一跑,那两个「孤儿」镜像就被正确识别成了两个已安装运行时的底层镜像。Mint 把它们放在「保留」里,并写明唯一能删掉它们的地方是 Xcode 的 Components 面板。要是给一个删除按钮,只会释放 21 MB,剩下的还会报成「什么也没移除」。
2026 年 10 月更新: 普通扫描已经不再让 CoreSimulator 重新认领它的存储,只读一遍登记表。这一步修复现在只在你通过 Mint 删除模拟器存储时才做。
Mint 现在根本不走 runtime add --move 这条路,也就不会再留下 8 GB 的暂存副本。CoreSimulator 已经标成 Unusable 的副本按 UUID 删除;留在 Inbox 里的暂存副本,会用管理员密码试一次。如果用户输入了管理员密码,删除仍然被拒,Mint 会记下来,在文件或系统有变化之前不再显示这一行。
这些坑都不算 macOS 的 bug。封印的系统卷、firmlink、写时复制克隆、延迟回收、会自己修复的运行时登记表,设计上都说得通,平时用 Mac 也感觉不到。可一个程序要告诉你空间去哪了,它们就会出问题,因为每一个都会让「目录占多大」和「删掉能拿回多少」对不上。
做磁盘清理工具最省事的办法,是遍历目录树、按大小排序,然后在大的东西旁边放个勾选框。碰上上面这些坑,这种做法显示的数字都会比能拿回的大,用户要等点了删除才会发现。
代价最大的两次失误,都跟测量误差无关。第一次是登记表说「没有任何东西引用它」,我们就信了,没先确认它是不是已经数完了。第二次是登记表显示删除成功,我们就以为命令起作用了,没去看磁盘上到底有没有字节被释放。
这些数据都是 2026 年 8 月在一台开发者 Mac 上用 Mint 的只读扫描测的,你也可以在自己的机器上复现。想看 coding agent 会留下什么,可以读另一篇:你的 Coding Agent 正在吃掉你的硬盘。
在自己的 Mac 上怎么判断
现在能用的操作步骤,请看 iOS 模拟器存储指南,里面把运行时、设备数据和缓存分开讲。如果大头是 DerivedData,就改看 Xcode 构建产物指南:那是另一类数据,删掉的后果也不一样。