让 Mole 的卸载列表点开就出来
在 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 行。
有自己名字的兄弟目录还是各占一行,比如 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 秒左右,所以最后都是在现有的扫描上把重复的事去掉。卸载之前怎么看这份残留列表,可以接着看卸载前先看一眼残留。