
众所周知,通过读取 USN 日志来搜索文件,比起传统的递归遍历文件夹,效率要高得多。但想要达到像 “Everything” 那样极致的速度,其实并不简单——哪怕只是几十毫秒的延迟,用户体验也可能天差地别。
如今,“Everything” 的界面风格略显经典,某些操作习惯也难以高度自定义。想必很多用户都希望有一款既能极致高效,又完全契合个人使用习惯的搜索工具:比如更灵活的快捷键、更丰富的右键菜单和后续操作联动等。正因如此,我决定重新开发这样一款工具,并将整个开发过程中的思路与遇到的“坑”记录下来。最终代码与实现已完全开源。
整个项目可分为两大部分:数据处理和UI 交互。
数据处理流程:
•a. 从磁盘缓存中读取文件信息•b. 将数据加载至内存•c. 执行字符串匹配•d. 返回匹配结果
UI 交互流程:
•a. 响应用户输入•b. 触发搜索并获取结果•c. 更新界面显示
我们通过读取 USN 日志获取文件信息。每条记录应至少包含:文件索引 ID(FileReferenceNumber, ulong)、父级索引 ID(ParentFileReferenceNumber, ulong)和文件名(含文件夹名称)。注意,USN 并不直接提供完整路径,我们需要根据父级索引逐级拼接出真实路径。
另一个关键是增量更新。USN 能记录文件状态变化(如创建、删除、重命名),通过监听如 USN_REASON_FILE_CREATE | USN_REASON_FILE_DELETE | USN_REASON_RENAME_NEW_NAME 这类事件,可实现高效的数据更新,远胜于传统的文件监控方式。
最基础的搜索方式是遍历所有字符串,但这样性能远远不够。我尝试了以下方法:
•建立索引机制:采用类似布隆过滤器的结构,使用 ulong 类型做初步筛选,通过预先的"或"运算初筛,实测可提升 2–3 倍搜索速度。
public static ulong TBS(string txt)
{
ulong indexValue=0;
for (int i = 0; i < SCREENCHARNUM; i++)
{
if (txt.Contains(alphbet[i], StringComparison.OrdinalIgnoreCase))
{
SetBit(ref indexValue, i);
}
else
{
ClearBit(ref indexValue, i);
}
}
return indexValue;
}•多线程搜索:将数据分组并用多线程并行处理。但在实际测试中(数据量 500W+),由于需频繁加锁同步结果,性能提升微乎其微,反而增加了代码复杂度。•拼音首字母支持:本项目加入了拼音首字母检索功能。由于实现较为直接,也带来了一定的匹配开销。如果去掉该功能,搜索速度还能进一步提升。
搜索结果使用定长数组存储,每次搜索只顺序更新前若干条匹配结果的引用和总数量,几乎没有额外开销。
UI 响应设计的重要性不亚于数据处理,甚至各占半壁江山。本项目先后尝试了 WinForms 和 Avalonia 两种 UI 框架,最终选择了支持 AOT 发布的 Avalonia,它在保持相近性能的同时,界面效果更现代。
我们将“用户输入”和“字符串匹配”分别放在不同线程中执行,通过异步信号机制触发或取消搜索任务,坚决避免阻塞 UI 线程。搜索任务运行在一个常驻循环中,避免因反复创建线程带来的开销。
面对海量数据的实时展示,虚拟化 UI 是必须的。
•在 WinForms 中,可使用 ListView 的虚拟模式动态生成条目。•在 Avalonia 中,我使用的是 ListBox 的虚拟化方案,并结合 ReactiveUI 框架,将 DataContext 绑定到一个可变长数据模型上。每次搜索结果更新时,通过 IEnumerable 接口延迟生成显示项,实现流畅渲染。
public class DataViewModel<T> : ReactiveObject
{
private IList<T> _allData = [];
private IEnumerable<T> _displayedData = [];
private int _displayCount = 100;
public IEnumerable<T> DisplayedData
{
get => _displayedData;
private set => this.RaiseAndSetIfChanged(ref _displayedData, value);
}
public int DisplayCount
{
get => _displayCount;
set
{
_displayCount = value;
UpdateDisplayedData();
}
}
public DataViewModel()
{
}
public void Bind(IList<T> _allData)
{
if (this._allData != _allData)
{
// 生成测试数据(实际中可能从文件或数据库加载)
this._allData = _allData;
UpdateDisplayedData();
}
}
private void UpdateDisplayedData()
{
// 使用 LINQ 的 Take(),这是惰性求值的,性能很好
DisplayedData = _allData.Take(DisplayCount);
}
// 快速切换到不同数量级
public void SetDisplayCount(int count)
{
if (DisplayCount != count)
{
if (count < 0) count = 0;
DisplayCount = count;
}
}文件路径和图标的获取都是动态完成的(可适当引入缓存)。最初图标采用异步加载,但会出现闪烁现象,后来改为同步获取,效果反而更稳定。
public string FileName => PathHelper.getfilePath(fileName).ToString();
public string FilePath => PathHelper.GetPath(this).ToString();在此基础上,我们还实现了诸如全局热键、搜索历史记录、自定义右键菜单等扩展功能,进一步提升实用性和个性化程度。
如果你对这款工具有任何建议或想法,欢迎随时交流!项目已在 GitHub 完全开源,如果你觉得有用,欢迎点个 Star ⭐️支持一下! https://github.com/LdotJdot/TDS[1]
[1]: https://github.com/LdotJdot/TDS