# 让 Mole 的卸载列表点开就出来

> 点一个 App 要等一两秒，时间花在了哪，哪些检查挪到了后台，以及 ChatGPT 的一万五千个临时文件夹为什么合成了一行。

Published: 2026-10-28

在 Mole 的卸载页里点一个 App，右边会先空一下，再出现「正在扫描」，过半秒到一秒残留才列出来，Xcode 每点一次要等三四秒，ChatGPT 要六秒，展开 ChatGPT 往下滚还会一顿一顿的。我想认真看一下这段时间到底花在了哪，顺便也翻了几个开源卸载工具是怎么做的，这些改动在 10 月 5 日做完，已经随 Mole 1.16.0 正式版发布，下面是当时的实测和取舍。

## 慢的不是 App 本身

一开始以为 App 越大越慢，测下来并不是，一个 6.6 MB、只有六项残留的小工具，第一次点开也要 0.56 秒左右。时间主要花在每次点击都要做的几件固定的事上，下面是在我的 M5 Pro 上用 Release 版本测的，取三次的中位数：

- 读取 Homebrew 的 cask 目录，这份目录有 5.6 MB，每次大约 200 ms，用来判断这个 App 是不是用 Homebrew 装的。
- 校验 App 的代码签名，而且校验了两次，大约每 MB 可执行文件 1 ms，MindNode 每点一次就要 260 ms。
- 最慢的一项扫描是最近使用的文稿列表，775 条记录大约 230 ms，几乎都花在逐条检查路径上。

个别 App 还有自己的情况叠在上面，Xcode 的安装收据有 41 MB，记着 12.5 万条路径，每次点开都要重新解析一遍，ChatGPT 会留下大约 15000 个临时文件夹，每一个都要单独确认是不是它的。

## 改了哪些地方

- cask 目录在打开卸载页时就在后台读好，不用等到点击。
- 点击时只读签名信息、不做校验，并记下这个 App 包的文件标识，完整的签名校验放到列表出来以后在后台跑，删除之前必须跑完。
- 最近使用的文稿先按名字筛一遍，名字对得上的才做完整的归属检查。
- 资源库下的几个根目录并行列出，容器的身份按元数据文件缓存，辅助程序的标识前缀每次扫描只算一次。
- 安装收据按收据文件缓存，第二次点 Xcode 不用再解析那 41 MB。
- 名字本身就能证明归属的临时文件夹不再逐个确认，真正删除时仍然逐个路径检查。
- 耗时长的扫描先开始，几百个小任务排在后面。
- 鼠标划过时开始的预扫描，指针一离开就停下，卸载之前开始的扫描，也不会在卸载以后再把旧结果写回来。

签名校验这一条，最省事的做法是直接删掉，我没删，只是挪到了点击以后。现在后台校验没通过时，App 本身照常卸载，只有靠签名信息才找到的那几类残留会留下来，比如按开发者团队 ID 匹配到的容器，结果里会标成「无法确认 App 签名」，没删也不算失败。如果扫描以后 App 被换成了另一个，仍然会拒绝卸载。

## 改完以后快了多少

打开卸载页大约 1.5 秒后第一次点击，同样是 Release 版本：

| App | 改之前 | 改之后 |
| --- | --- | --- |
| Shottr | 556 ms | 137 ms |
| MindNode | 1233 ms | 169 ms |
| Raycast | 716 ms | 171 ms |
| Parallels Desktop | 871 ms | 194 ms |
| VS Code | 584 ms | 232 ms |
| 微信 | 605 ms | 238 ms |
| Xcode | 3689 ms | 384 ms |
| ChatGPT | 6134 ms | 1530 ms |

大多数常见 App 现在都在 250 ms 以内，残留一次列出来，不会先闪一下「正在扫描」，后台的签名校验在列表出来以后 12 到 434 ms 内跑完。

## ChatGPT 那一万五千行

速度上来以后，展开 ChatGPT 往下滚还是卡，数了一下，它列出了 15175 行，其中 15143 行是 `T/.com.openai.codex.XXXXXX` 这种临时文件夹，一共 1.0 GB，App 每次启动都会留下几个。我自己做的 Hina 也类似，344 行里有 323 行是测试跑完留下的 `Hina-<Suite>-<UUID>.plist`，加起来才 0.01 MB，两边都是一种随机命名的东西占满了整个列表。

卡顿除了行数多，还有两个原因，展开的那一行每次重绘都要把所有残留重新排一遍序，鼠标停在列表上、行从指针下面滑过时，又会触发整行重绘，于是每滑过一行就再排一次。拿真实的 ChatGPT 数据测，一次重绘从 309 到 354 ms 降到 56 到 58 ms，Hina 从 33 到 36 ms 降到 17 到 19 ms，ChatGPT 展开后的高度也从 3200 pt 缩到了 1225 pt。

现在几行残留如果是同一类、在同一个目录、归属同一个 App，默认勾选也一样，名字只差 App 自己生成的那一段，就会合成一行，这一段可以是随机后缀、UUID、一长串十六进制或者崩溃报告里的时间戳。合并后的那一行保留类别名，路径里用 `*` 代替变化的部分，后面写上数量和总大小，只有一个勾选框，也不再展开，ChatGPT 变成 13 个类别 33 行，Hina 变成 24 行。

<figure class="blog-diagram">
  <picture>
    <source media="(max-width: 900px)" srcset="/img/blog/uninstall-leftovers-one-line-mobile-zh.webp">
    <img src="https://mole.fit/img/blog/uninstall-leftovers-one-line-zh.webp" width="1600" height="992" loading="lazy" alt="同一份 ChatGPT 残留的前后对比，改之前每个临时文件夹各占一行，改之后合成一行，写着 15,143 项、1.02 GB，其余残留按类别列出。">
  </picture>
  <figcaption>同一份 ChatGPT 残留，改之前一个临时文件夹一行，改之后合成一行。</figcaption>
</figure>

有自己名字的兄弟目录还是各占一行，比如 ChatGPT 的七个 Electron 缓存 `GPUCache`、`Code Cache`、`Crashpad` 这些，把它们也合并的话，Hina 只剩七行左右，看着反而不像清干净了，十几行是比较合适的量。像 `T/<id>.helper` 这种名字里带个六个字母单词的，也单独一行，因为只看这六个字母，分不清它是不是随机后缀。

之前试过的两种做法这次也没再用，一种是在已经展开的 App 里再套一层可以折叠的分组，9 月 30 日试过，10 月 2 日就删掉了，折叠里套折叠，相似的路径还会被分到分组和散行两边，另一种是一个类别只放一行，9 月 4 日用过，一周后又改回来，因为它把不同位置的东西藏到了一起。

## 别的卸载工具怎么做

Pearcleaner 会并发列出大约 62 个根目录，只按名字匹配，不确认归属，也没有缓存，它快是因为做得少，腾讯柠檬在进入页面时就把所有 App 的残留算好，每天刷新一次，点击自然是瞬间的，代价是每次打开都有一次很大的后台扫描，和 Mole 不新增常驻开销的方向不合，MangoDisk、Uninstally 和 Mole CLI 用的是固定的根目录加有限的并发。

翻下来没有哪个工具有比 Mole 已经在用的 `getattrlistbulk` 和 `readdir` 更快的底层办法。Spotlight 我也试过，查一次要 0.3 到 0.4 秒，还漏掉了大半残留，VS Code 的四项只找到一项，`searchfs` 在 APFS 上能用，但一次要 15 秒左右，所以最后都是在现有的扫描上把重复的事去掉。卸载之前怎么看这份残留列表，可以接着看[卸载前先看一眼残留](https://mole.fit/zh/blog/mac-uninstall-leftovers-review-before-delete)。

---

Canonical HTML page: https://mole.fit/zh/blog/mole-uninstall-review-speed
Blog index for agents: https://mole.fit/zh/blog/llms.txt
Site index for agents: https://mole.fit/llms.txt
