
我电脑又红了。不是股票是磁盘。df -h一看根目录 100%当时心里就咯噔一下——又得开始那套“考古式”清理了。搞开发的都懂电脑里最不值钱又最占地方的就是临时文件但最让人头疼的也是它们。今天不聊那些大而全的系统优化软件就聊聊我这个专门给开发者用的临时文件清理思路和落地工具以及我是怎么把它从“能用”做到“好用”的。1. 内容整体设计与思路拆解1.1 这玩意儿到底解决了什么问题先认清一个现实普通用户清理临时文件无非是清浏览器缓存、回收站这些用系统自带功能就行。但开发者不一样我们的机器里藏着一堆“变相临时文件”编译缓存、构建产物、IDE 索引、npm/yarn/pip 缓存、Docker 构建缓存、.gradle 目录、target 目录……名字里带 cache、temp、log、dist 的目录一个比一个能吃硬盘。最气人的是这些文件属于“留之无用弃之可惜”的类型。说没用吧能加速二次构建说有用吧占了几个 GB 却很少真的用到。而且它们散落在用户目录、项目目录、甚至 VM 虚拟机镜像里手动去找既费眼又费神还不安全——万一删错了得重新拉依赖、重建索引半天时间就搭进去了。所以这个工具的核心定位就一句话针对开发者的高频临时垃圾重灾区做一次规则驱动的精准清理。不是大而全的“垃圾扫描”而是“我知道你哪些东西可以删、哪些必须留着”的聪明方案。1.2 为什么我不用现成的“一键清理”软件市面上的清理软件我早年也用过后来全卸了。原因有三条条条都是痛点误杀率不低。很多清理软件为了凑“清理出多少 GB”这个数字经常把不该删的配置缓存也标记成垃圾。比如 pip 的 HTTP 缓存删了确实没事但 wheel 缓存删了之后下次重装包又是满速下载耽误时间。无差别清理缺乏场景感知。它不知道你正在跑哪个项目也不懂某个目录对应的工具链正在运行。清了正在使用的 IDE 索引目录轻则编辑器卡半天重建索引重则直接崩。对开发者目录结构理解太浅。它不会区分~/.gradle/caches里的modules-2和transforms-1哪个才是真正的缓存大头更不知道~/Library/Developer/Xcode/DerivedData能不能整目录干掉答案是能但大部分软件不敢动。所以答案很明确自己动手用规则引擎定义清理边界这才是开发者该有的解法。这套思路的核心不是“敢删”而是“知道什么时候不该删”。2. 核心细节解析与实操要点2.1 扫盘思路与规则体系设计我把临时文件按照“可清理的安全等级”分成了三层这是整个方案的地基A 类放心删永远不会被二次使用的缓存或日志。典型代表是各语言包管理器的下载缓存、IDE 的崩溃日志、系统临时目录里的旧文件。这类东西只占地方删了毫无副作用。B 类谨慎删“重建成本低”但“当前可能有用”的文件。比如编译中间产物.o、.class、target、构建缓存。删了之后下一次构建会自动重建但耗时要看项目大小。这类必须支持“保留最近 N 天的策略”只清老旧的。C 类不删当前正在用的配置、凭据、SDK 下载缓存、虚拟环境本身。比如~/.ssh、~/Library/Keychains、node_modules这种绝对不能碰。规则引擎按目录维度配置就行了核心就两个字段pattern路径匹配模式和policy删除策略{ rules: [ { name: pip-http-cache, pattern: ~/.cache/pip/**, policy: delete-all, max_age_days: 30 }, { name: gradle-wrapper-dists, pattern: ~/.gradle/wrapper/dists/**, policy: keep-latest, keep_count: 2 }, { name: xcode-derived-data, pattern: ~/Library/Developer/Xcode/DerivedData/**, policy: delete-if-older-than, max_age_days: 7 } ] }2.2 路径通配符与安全边界的隐藏坑这块我得重点说因为刚踩过。很多人扫盘喜欢用简单的find-name cache这种粗糙匹配然后一路删下去。但如果你把规则配得过于宽松很容易出现两个问题符号链接穿越。比如~/Library/Caches下如果存在指向重要目录的软链有些第三方库这么干你用递归删除 API 会顺着链接跑进去删真目录。所以匹配后必须再做一层lstat检查识别真实路径和软链目标位置。跨盘边界未校验。有些人把临时目录映射到外置 SSD 或者 RAM Disk 里我试过把 systemd-tmpfiles 配到内存盘。如果规则引擎扫描时没有存储设备边界判断可能会出现一个正则跨磁盘路径匹配到别的东西。安全做法是每个规则的pattern必须先 normalize 成绝对路径再用Path.is_relative_to()判断是否在允许的根目录内。这些细节如果不处理轻则清不干净重则把正在运行的服务的 socket 目录给删了直接闹线上事故。2.3 “智能”的底气三步判定逻辑我常说这工具不是靠“胆子大”删文件而是靠“信息完整”再做决策。执行清理前核心引擎会跑三步判定第一步进程占用检查。如果某个目录正被进程占用或者对应进程的 PID 属于当前活跃的 IDE、终端会话那么这个目录会被自动跳过。实现上就是枚举/proc下所有进程的 fd做路径前缀匹配macOS 上走lsofLinux 走/proc这套检查下来删之前就知道谁能动谁不能动。第二步时间窗口过滤。B 类文件默认保留 3 天的近期产物。比如你昨天刚 build 过一次那生成的文件不到 3 天就不动它。这个窗口根据项目类型调整前端项目通常 1 天就够大型原生项目 7 天更稳。第三步白名单覆盖。某些路径虽然规则匹配上了但实际内容特殊比如/tmp里挂着当前用户正在用的一些 Unix socket需要有一层白名单规则先拦截。我的习惯是“白名单优先级永远高于黑名单”宁可少删不可误删。3. 实操过程与核心环节实现3.1 手把手跑一遍完整清理流程我拿自己这台 MacBook Pro 做演示平时主要跑 iOS Flutter Python 三套东西是典型的“开发环境重度感染者”。先安装工具并初始化规则库# 假设这个工具叫 dev-cleaner brew install dev-cleaner # 初始化默认规则集内置了 150 条针对开发者的规则 dev-cleaner init # 先干跑一次只看分析结果不实际删除 dev-cleaner scan --dry-run输出的核心结构长这样═══════════════════════════════════════════ 扫描模式dry-run不删除 共发现 37 个可清理目录总计 18.6 GB [A类] 可直接清理 / 合计 11.2 GB - ~/Library/Caches/pip 748 MB - ~/Library/Caches/Homebrew 2.1 GB - ~/.gradle/wrapper/dists 1.8 GB [B类] 需谨慎处理 / 合计 6.8 GB - ~/Library/Developer/Xcode/DerivedData 4.2 GB - ~/.pub-cache/hosted 611 MB - ~/Library/Developer/CoreSimulator/Caches 1.1 GB [S类] 建议跳过保留 / 合计 512 MB - ~/Library/Developer/Xcode/iOS DeviceSupport 512 MB ═══════════════════════════════════════════3.2 导出清理白名单与执行策略扫描结果出来后我不会直接全选而是手动勾选要清理的类别生成一个允许清理的名单。重点来了这个名单会拼成一个 JSON 存进工具目录每次扫描它会自动读取下次直接一键盘执行。# 手动指定类别 dev-cleaner clean --categories A --include gradle-wrapper # 如果要连 B 类一起清加 --force-b 并设置保留窗口 dev-cleaner clean --categories A --include gradle-wrapper --force-b --keep-b-days 3真正执行时工具会先重新做一次快速扫描防止这期间有文件被改动然后逐目录执行删除前打印路径、确认归属规则、执行、回收磁盘空间、更新统计。我实测完整流程大概是重扫 3 秒 删除 40 秒性能瓶颈主要花在大目录的递归遍历上。3.3 定时清理与增量回收的配置逻辑搞开发的人都怕后台任务突然“作妖”所以我把定时清理的关键做成可控的小步增量。目前我的 crontab 是这样写的0 3 * * 5 /usr/local/bin/dev-cleaner clean --categories A --kept-b-days 5 --min-free-gb 20 ~/dev-cleaner.log 21每周五凌晨 3 点只清 A 类B 类需要至少 5 天以上未使用才动。注意--min-free-gb 20意思是磁盘剩余空间低于 20 GB 时这个任务才会执行不然即使时间到了也不会清理——主要是防止抛光磁盘写入寿命。定时清理最需要注意的坑是不要让定时任务在构建过程中跑。我踩过一次CI 构建跑到一半定时器把~/Library/Caches/Android底下的 sdk 缓存目录清了结果几个模块直接拉不到依赖构建失败一群人排查了半小时才发现是被清理任务截胡了。后来我加了分钟级锁定逻辑检测到gradle、mvn、xcodebuild等进程存在时直接跳过宁可这周不清理也绝不当拦路虎。3.4 Windows 与 Linux 下的目录差异适配说实话这个工具最初是给 macOS 写的但后来我移植到了 Windows 和 Linux 上。三套系统的临时文件分布差异很大直接套用规则会出事系统主要缓存路径备注Windows%LOCALAPPDATA%\npm-cache、%LOCALAPPDATA%\Yarn\Cache、%LOCALAPPDATA%\Temp需要走 Rust 的dirscrate 做路径映射注意npm从 v7 之后缓存路径有改动Linux~/.cache/pip/**、~/.cargo/registry/src/**、~/.nuget/packages这些目录被大量容器化工具共用规则必须保守macOS上述示例路径注意Xcode部分是重灾区删除前务必备份 DerivedData 的近 7 天内容适配步骤用环境变量扩展完成~会被解析成当前用户的 home${LOCALAPPDATA}在 Windows 上自动映射。跨平台的核心逻辑是同一个规则文件跑不同系统不报错也不误删这点我在 CI 里做了快照测试来兜底。4. 常见问题与排查技巧实录4.1 磁盘空间明明没释放是怎么回事清理完显示“已释放 3.2 GB”但df -h一看根本没变化。这个问题最常见的原因就是延迟分配和文件系统保留块。先说 Linux 上的ext4 默认给 root 保留 5% 空间tune2fs -m 0可以调这 5% 是算不到可用空间里的。如果你刚好卡在保留块边界上清理几个 GB 可能显示上没变化。再说 macOS 上的 APFS有一种情况是有进程还占着被删文件的 fd但 inode 没有释放空间要等那个进程关掉才真正收回。排查方法很简单先用lsof L1列出所有“被删除但仍打开”的文件确认是不是自己的终端还开着旧目录的 shell。4.2 误删了正在用的文件怎么办工具默认有保护机制所有删除操作执行前都会把文件路径写入“回收站清单”这个清单在工具目录下存一份删错后可以一键恢复。我的方案是做了软链接式的备份比如准备删~/.gradle/wrapper/dists/下某个版本目录我会先把它移动到dev-cleaner-trash/目录下并不会真的rm -rf等确认几天没问题后再由另一个命令物理删除。# 恢复最近一次被“移动”到回收站的目录 dev-cleaner restore --latest --to ~/.gradle/wrapper/dists/误删了也能恢复这个设计是整套方案让我睡得着觉的核心。不夸张地说没有这个回收站机制我不敢用任何清理工具。4.3 扫描慢如蜗牛如何优化先看一眼自己的目录总量。我见过有人把整个 Homebrew 源码目录塞在~/homebrew下扫一次要跑 20 分钟。核心的优化手段是白名单排除 并行遍历把不需要扫描的大目录比如~/node_modules、~/venv加到 exclude 列表里扫描时间能减少 70%。用并行 I/O 遍历配合内存里维护的目录大小 Map。我用 Rust 的ignorecrate 做遍历单机一次全量扫描的控制时长基本在 1 分钟以内效果稳定。如果目录实在太多可以先扫“高价值目录”再跑后台增量扫描第一次清理优先处理已知大头。后来我加了一个小技巧保留上一次扫描的结果缓存在内存里如果规则和目录结构都没变后续扫描直接读上次的元数据整个过程不到 1 秒速度体感好很多。4.4 常见问题速查表症状可能原因解决步骤释放的空间比预期少有进程占用删除文件lsof L1查找并重启对应进程某个规则始终跳过白名单优先级更高检查规则文件的 preflight 列表npm缓存清了还是满只清了 cache 目录没清_logs和孤儿包追加~/.npm/_logs/**规则清完构建变慢很多B 类文件被误清导致缓存冷启动下次执行加--keep-b-days 10拉长窗口扫不到 Docker 缓存运行的是 rootless 模式路径在用户目录单独配置~/.local/share/docker规则清理后df数值有变化但系统报错磁盘满APFS 的 Snapshot 占用用tmutil listlocalsnapshots检查并清除本地快照4.5 踩过几次坑之后留下的“避雷针”最后分享一个原则也是我使用这套工具将近两年、累计清理了几百 GB 之后沉淀下来的经验清理工具的配置永远要比你当前的磁盘需求“保守一档”。比如我现在剩余空间 60 GB我会把清理阈值设定成“低于 40 GB 才考虑动 B 类”这样即使遇到一次误判损失也在可接受范围内。另外规则文件本身记得纳入版本管理。我把它丢在 dotfiles 仓库里每次改规则都是 commit PR 流程这样万一出了事可以顺着提交记录快速定位是哪个规则踩雷。毕竟临时文件清理核心不是技术难度而是边界意识——你永远不知道用户的开发环境里哪个看似无关紧要的目录正被某个关键服务当命根子用。所以尽量别碰边界碰了也要留下后悔药。