- 问题: 一台开发者的 Mac 把 30.18 GB 存了两份。大部分在别的应用的容器和源码仓库里,在那里删副本并不安全。
- 我们发现: 靠删文件腾空间的工具,能诚实提议的只有其中大约 3%。APFS 可以让两个相同的文件共用数据块,两个文件都留着,空间也回来了。
- 我们做了什么: 一个能在整个家目录里让副本共用存储的扫描。第一次全盘执行用 28 秒处理了 8,414 个副本,拿回 17.21 GB,事后文件全部通过校验。
找到重复文件,并不能直接告诉你哪份能删。即使别处有相同内容,项目也可能仍然需要当前路径下的这一份。我们在一台 Mac 上测出 30.18 GB 重复存储,再验证 APFS 在哪些情况下能保留两个文件、同时减少占用。你的 Mac 能省多少,取决于文件本身以及它们是否已经共享数据块。
为了弄清一台开发者的 Mac 是被什么塞满的,我们走完了它的整个家目录:523,217 个文件,按实际占用的空间分组,大小相同的拿去做哈希,逐字节比对候选,再把已经共用磁盘块的那些排除掉。剩下的是 30.18 GB 被这台机器存了两份,分布在 44,830 个组里。
我们本来可以到此为止,在页面上写个 30 GB。真正改变了产品的,是把同一份普查换个角度再读一遍。
先数清实际存在的字节
在现在的 Mac 上,最顺手的算法是错的,而且错得刚好让存储工具显得很能干。
文件的大小和它占的空间是两个数。那个家目录按大小是 670.0 GB,按磁盘上实际占用是 225.5 GB。差额大部分来自 14,759 个占位文件,是 iCloud 之类的同步客户端留在本地的:有名字、有图标、有大小,背后没有字节。把大小加起来的工具,会向你承诺几百 GB 根本不存在的空间;删掉一个占位文件,等于拿走你唯一的那份副本,却一个字节也没释放。
所以这篇里的每个数字都是实际分配的字节。这样算出来的总数小一些,但你能拿自己的可用空间去核对。
30 GB 的两种读法
第一种读法看这些副本是什么。最大的一块是 13.06 GB 的 AI 模型权重,其中一个 2.9 GB 的模型文件同时躺在四个地方:一个应用的容器、一份测试固件、一份备份、一个支持目录。接下来是其他应用数据、构建产物,然后是媒体和文档。
第二种读法看它们在哪儿,后面的一切都是这一种决定的。13.51 GB 在别的应用的容器里,12.69 GB 在源码仓库里。落在"清理工具可以开口建议删除"的目录里的,不到 1 GB。
靠删文件来腾空间的工具,能诚实地向你提议的,大约只有它刚找到的 3%。没人希望清理工具去建议删掉另一个应用容器里的东西,或者你还在写的源码树里的东西。我们当然可以做一个"报告 30 GB、能动的只有 1 GB"的扫描器,市面上这样的不少。
动作决定了范围
APFS 可以让两个文件共用同一份数据块。这个调用叫 clonefile,你在 Finder 里"复制"一个文件时用的就是它,不花时间,也不多占空间。两个文件都还在,名字、所在文件夹、权限、时间戳和内容都是各自的,共用的只是底下的存储。
在一个 200 MB 的文件上实测,数字是这样变的:
它们开始共用的那一刻,空间就回来了,两个文件谁都没察觉。
这个动作没有风险,所以范围可以是整个家目录,不必局限在那几个"可以提议删除"的文件夹。最后这张网比重复文件检测器宽了大约三十倍,原因只有一个:动作不再是删除。
有一条规矩从头守到尾:哈希永远不决定任何一次写入,它只负责缩小范围。写任何东西之前,两个文件都要逐字节比对一遍,所以哈希碰撞不可能让任何人丢数据。
整个家目录 870,229 个文件,第一次扫描用 7.3 秒,之后 5.4 秒,最早的版本要 59.8 秒,所以每次点击都能跑,不用定时跑。
一个你可以自己核对的承诺
存储工具里满是没人能验证的数字。这个有回执,而且能用三套互不相干的机制来核对。
承诺值是执行前界面上那些行加起来的和。回执是执行后逐个文件重新读取私有字节测出来的。df 是卷自己的账,对这一切毫不知情。三个数一旦对不上,就说明承诺错了,我们宁可在这里发现,也不想让谁在自己的硬盘上发现。
然后我们核对文件本身,依据的是执行前就已存在、我们也从不写入的记录:
- 3,478 个被 Git 跟踪的副本,现在的哈希仍然等于 Git 当初记录的 blob。零个不一致。
- 1,877 个 PNG 通过了自己的内部分块校验和,77 个磁盘映像通过了自己的数据分叉校验,11 个 SQLite 数据库通过完整性检查,每一个 ZIP 和 gzip 条目都通过了 CRC。
- 164 个 Mach-O 签名有效,54 个已签名的 app 包保住了各自的
codesign结论和代码目录哈希,20 个调试符号包保住了 UUID。 - 两个经过公证的 app 副本,Gatekeeper 的结论和之前一模一样。
一个共用了数据块的文件,仍然是它原来那个文件。上面这些核对,就是我们敢这么说的依据。
第一次真实执行暴露的两个错
它改动了两样不该改的东西:Finder 的"添加日期",以及第一次全盘执行时 8,414 个副本里有 301 个丢了"由哪个应用下载"的记录。现在 Mint 会在前后逐项比对每个文件的属性,除了占用的空间,文件的一切都不变。
量过但没有做的那个方案
macOS 有一个透明压缩的标记,在这台机器上,这笔账很诱人:全盘采样下来,压缩能拿回 57.5 GB,比共用数据块还多,而且文件照常可用。Apple 自己在系统里大量用它。
如果你在跑 coding agent,说的就是你这台机器
那 13.06 GB 重复的模型权重,就是跑 agent 的机器常见的样子。不跑 agent 的 Mac 很少会这样。
Agent 本来就会到处复制东西。一个模型给应用拉一次,再往它的沙盒容器里拉一次。Agent 建的每个 worktree 都带着自己的一份构建产物。一份测试固件里放着它要比对的那个文件的副本。这些都不是错,也都不是你想手动去删的,因为你没法每次都判断出谁依赖着哪一份。
共用数据块在这种情况下最合适。你不用知道哪一份重要,因为它们全都照常能用;也不用退出 agent,更不用做任何决定。第一次全盘执行:3,262 组,8,414 个副本,28 秒,零个被拒,拿回 17.21 GB。执行完再扫一次,只剩 37 MB,是被当时正在运行的应用占着的。
这件事教会我们的
总量从来不是有用的那个数,按位置的拆分才是。它说明"扫描器能报出来的数"和"清理器能动手的数"是两个数,差了三十倍。后面的一切,都来自换掉动作这个决定,而不是去要更大的权限。
还有一点:工具既然承诺能腾出空间,事后就该拿得出证据。在一次动了某个人 8,414 个真实文件的执行里,承诺、回执和卷本身的账,三个数精确到字节一致。
判断副本是否应该保留
用重复文件指南分清哪些文件可以移除、哪些副本还得留在原处继续用。如果只是 System Data 数字很大,先看 System Data 排查指南:光看分类总量,看不出该删什么。