企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# Windows 10 - 通过搜索索引器加快文件操作速度 作者 [Adam Wilson](https://msdn.microsoft.com/zh-cn/magazine/mt149362?author=Adam+Wilson) | 2015 年 11 月 很多 Windows 版本现在都包含搜索索引器,它可以为一切项目提供支持(从文件资源管理器中的库视图到 IE 地址栏),并能为“开始”菜单和 Outlook 提供搜索功能。在 Windows 10 中,搜索索引器不再仅用于桌面计算机,现可用于所有通用 Windows 平台 (UWP) 应用。尽管这还能使 Cortana 更好地运行搜索,但最令人激动的是,这项提升大大改进了应用与文件系统的交互方式。 借助索引器,应用可以执行更多有趣的操作,如对文件进行排序和分组,以及跟踪文件系统中的更改。大多数索引器 API 都可通过 Windows.Storage 和 Windows.Storage.Search 命名空间用于 UWP 应用。应用已在使用索引器带来绝佳用户体验。在本文中,我将逐步介绍如何使用索引器跟踪文件系统中的更改、快速呈现视图,并提供了一些关于如何改进应用查询的基本提示。 ## 快速访问文件和元数据 大多数用户设备都包含数百或数千个媒体文件,其中包括用户最珍爱的图片和喜爱的歌曲。可以快速循环访问设备上的文件并能与文件进行激励式交互的应用是所有平台上最受欢迎的应用。UWP 提供一系列类,可用于访问任意设备上的文件,无论其外形规格如何。 Windows.Storage 命名空间包括可访问文件和文件夹的基本类,以及大多数应用可执行的基本操作。不过,如果应用需要访问大量文件或元数据,那么这些类就无法提供用户所需的性能特征了。 例如,如果您在枚举时没有控制文件夹,则调用 StorageFolder.GetFilesAsync 会后患无穷。用户可以将数十亿个文件放置在一个目录中,但尝试为每个文件都创建 StorageFile 对象则会导致应用迅速内存不足。即使在不太极端的情况下,这种调用的返回速度也仍会非常慢,因为系统需要创建数千个文件句柄,并将它们封送回应用容器。为了帮助应用避免出现这种问题,系统提供 StorageFileQueryResults 和 StorageFolderQueryResults 类。 每当您编写应用来处理大量文件时,StorageFileQueryResults 都是首选类。不仅是因为它能提供简便的方式来枚举和修改复杂搜索查询的结果,也因为 API 将枚举请求视为“*”查询。它同样适用于更加单调的日常情况。 使用索引器(如果有)是加快应用运行速度的第一步。现在,这句话出自索引器项目经理之口,听上去好像是出于自身利益考虑(让我自己不下岗)的托辞,但我这样说确实是有符合逻辑的理由的。StorageFile 和 StorageFolder 对象在设计时就考虑到了索引器。在对象中高速缓存的属性可从索引器快速检索。如果您没有使用索引器,系统必须从磁盘和注册表中查找值,而这一操作是 I/O 密集型操作,会引起应用和系统的性能问题。 为了确保索引器会得到使用,请创建 Query­Options 对象,并将 QueryOptions.IndexerOption 属性设置为要么仅使用索引器: ~~~ QueryOptions options = new QueryOptions(); options.IndexerOption = IndexerOption.OnlyUseIndexer; ~~~ 要么使用可用的索引器: ~~~ options.IndexerOption = IndexerOption.UseIndexerWhenAvailable; ~~~ 如果缓慢的文件操作不会锁定您的应用,也不会削弱用户体验,则推荐使用 IndexerOption.Use­IndexerWhenAvailable。这会尝试使用索引器来枚举文件,但也会回退到更加缓慢的磁盘操作速度(如果需要的话)。如果不返回任何结果优于等待缓慢的文件操作,则最好使用 IndexerOption.OnlyUseIndexer。如果索引器遭到禁用,则系统会返回 0 个结果,但返回的速度会非常快,仍能让应用响应用户。 有时,创建 QueryOptions 对象对于仅快速枚举来说似乎有点过分,在这种情况下,担心索引器是否存在可能并无意义。在您控制文件夹内容的情况下,可以调用 StorageFolder.GetItemsAsync。这是易于编写的代码行,如果目录中只有几个文件,那么所有性能问题都会被隐藏。 另一种加快文件枚举速度的方法是,请勿创建不必要的 StorageFile 或 StorageFolder 对象。即使使用的是索引器,打开 StorageFile 也需要系统创建文件句柄,收集一些属性数据,然后将它封送回应用进程。此 IPC 会带来固有延迟,在很多情况下,可以避免这种延迟,只需从一开始就不要创建这些对象即可。 需要注意的重要事项是,索引器支持的 StorageFileQueryResult 对象不会在内部创建任何 StorageFiles。它们是通过 GetFilesAsync 应请求创建。在此之前,系统在内存中只保留文件列表(相对轻型)。 枚举大量文件的推荐方法是,对 GetFilesAsync 使用批处理功能,根据需要传入多组文件。这样,您的应用可以在等待创建下一组文件时,对文件进行后台处理。图 1 中的代码通过简单的示例对此进行了展示。 图 1:GetFilesAsync ~~~ uint index = 0, stepSize = 10; IReadOnlyList<StorageFile> files = await queryResult.GetFilesAsync(index, stepSize); index += 10;           while (files.Count != 0) {   var fileTask = queryResult.GetFilesAsync(index, stepSize).AsTask();   foreach (StorageFile file in files)   {     // Do the background processing here      }   files = await fileTask;   index += 10; } ~~~ 这是 Windows 上的大量现有应用已使用的同一编码模式。通过改变步长,它们可以提取正确数量的项目,以在应用中显示响应第一的视图,同时在后台快速准备剩余文件。 属性预取是另一种可以加快应用运行速度的简单方法。通过属性预取,您的应用可以通知系统自己关注某组给定的文件属性。系统会在它枚举一组文件时从索引器中获取这些属性,并将它们高速缓存在 StorageFile 对象中。相对于在文件返回时挨个获取属性,这样做能很轻松地提升性能。 在 QueryOptions 对象中设置属性预取值。通过使用 Property­PrefetchOptions,可以支持一些常见的方案,但应用还能将请求的属性自定义为 Windows 支持的任意值。执行此操作的代码非常简单: ~~~ QueryOptions options = new QueryOptions(); options.SetPropertyPrefetch(PropertyPrefetchOptions.ImageProperties,   new String[] { }); ~~~ 在此示例中,应用使用图像属性,不需要其他任何属性。在枚举查询结果时,系统会在内存中高速缓存图像属性,以便稍后这些图像属性可以快速可用。 最后需要注意的一点是,属性必须存储在预取索引中,才能提升性能;否则,系统仍必须访问文件才能找到值,这样相对来说非常缓慢。属性系统的 Microsoft Windows 开发者中心页面 ([bit.ly/1LuovhT](http://bit.ly/1LuovhT)) 介绍了 Windows 索引器上可用属性的所有信息。只需在属性描述中查找 isColumn = true 即可(这表示属性可供预取)。 有了所有这些改进,您的代码运行速度会更快。来看一个简单的示例,我编写了一个应用,用于检索我的计算机中的所有图片及其垂直高度。这是照片查看应用要显示用户照片集必须采取的第一步。 我运行了三次,以尝试不同的文件枚举方式,并显示它们之间的差异。第一次测试在启用了索引器的情况下,使用了简单代码,如图 2 所示。第二次测试使用了图 3 中所示的代码,可执行属性预取和文件传入。第三次测试是在禁用了索引器的情况下,使用属性预取和文件传入。此代码与图 3 中的代码几乎相同,只是更改了一行代码,如注释中所示。 图 2:枚举库的简单代码 ~~~ StorageFolder folder = KnownFolders.PicturesLibrary; QueryOptions options = new QueryOptions(   CommonFileQuery.OrderByDate, new String[] { ".jpg", ".jpeg", ".png" });           options.IndexerOption = IndexerOption.OnlyUseIndexer; StorageFileQueryResult queryResult = folder.CreateFileQueryWithOptions(options); Stopwatch watch = Stopwatch.StartNew();           IReadOnlyList<StorageFile> files = await queryResult.GetFilesAsync(); foreach (StorageFile file in files) {                   IDictionary<string, object> size =     await file.Properties.RetrievePropertiesAsync(     new String[] { "System.Image.VerticalSize" });   var sizeVal = size["System.Image.VerticalSize"]; }            watch.Stop(); Debug.WriteLine("Time to run the slow way: " + watch.ElapsedMilliseconds + " ms"); ~~~ 图 3:枚举库的优化后代码 ~~~ StorageFolder folder = KnownFolders.PicturesLibrary; QueryOptions options = new QueryOptions(   CommonFileQuery.OrderByDate, new String[] { ".jpg", ".jpeg", ".png" }); // Change to DoNotUseIndexer for trial 3 options.IndexerOption = IndexerOption.OnlyUseIndexer; options.SetPropertyPrefetch(PropertyPrefetchOptions.None, new String[] { "System.Image.VerticalSize" }); StorageFileQueryResult queryResult = folder.CreateFileQueryWithOptions(options); Stopwatch watch = Stopwatch.StartNew(); uint index = 0, stepSize = 10; IReadOnlyList<StorageFile> files = await queryResult.GetFilesAsync(index, stepSize); index += 10; // Note that I'm paging in the files as described while (files.Count != 0) {   var fileTask = queryResult.GetFilesAsync(index, stepSize).AsTask();   foreach (StorageFile file in files)   { // Put the value into memory to make sure that the system really fetches the property     IDictionary<string,object> size =       await file.Properties.RetrievePropertiesAsync(       new String[] { "System.Image.VerticalSize" });     var sizeVal = size["System.Image.VerticalSize"];                      }   files = await fileTask;   index += 10; } watch.Stop(); Debug.WriteLine("Time to run: " + watch.ElapsedMilliseconds + " ms"); ~~~ 查看带预取和不带预取分别得到的结果,性能差异非常明显,如图 4 所示。 图 4:带预取和不带预取分别得到的结果 | 测试用例(桌面计算机上有 2,600 张图像) | 超过 10 个示例的平均运行时 | |---|---|---| | 简单代码 + 索引器 | 9,318 毫秒 | | 所有优化 + 索引器 | 5,796 毫秒 | | 优化 + 无索引器 | 20,248 毫秒(48,420 毫秒冷状态) | 通过应用此处所概述的简单优化,可以令简单代码的性能几乎翻一番。这些模式也都经过实践检验。在发布任何 Windows 版本之前,我们都会与应用团队协作,以确保照片、Groove 音乐和其他内容都能尽可能地良好运行。这就是为什么会有这些模式的原因所在,它们直接复制自 UWP 上的首批 UWP 应用的代码,并可以直接应用于您的应用。 ## 跟踪文件系统中的更改 如此处所示,在一个位置枚举所有文件是一项资源密集型进程。大多数情况下,您的用户不会关注旧文件。他们希望查看刚刚拍摄的图片、播放刚刚下载的歌曲,或者查看最近编辑的文档。为了有助于将最新的文件置于顶端,您的应用可以跟踪文件系统中的更改,并能轻松找到最近创建或修改的文件。 跟踪更改的方法有两种,具体取决于您的应用是位于后台还是前台。当应用位于前台时,它可以使用 StorageFileQueryResult 对象的 ContentsChanged 事件,根据给定查询收到更改通知。当应用位于后台时,它可以注册 StorageLibraryContentChangedTrigger,以便在更改发生时收到通知。这两种方法都会发送通知,让应用知道发生了更改,但其中并不包括已更改文件的相关信息。 若要查找最近修改或创建的文件,请使用系统提供的 System.Search.GatherTime 属性。此属性针对索引位置上的所有文件进行设置,并跟踪索引器最后一次注意到文件修改的时间。不过,此属性会基于系统时钟不断进行更新,因此,涉及时区切换的常规免责声明、夏令时以及用户手动更改系统时间仍然适用于在您的应用中信任此值。 在前台注册更改跟踪事件非常简单。在创建覆盖了应用关注范围的 StorageFileQueryResult 对象后,只需注册 ContentsChanged 事件即可,如下面的代码所示: ~~~ StorageFileQueryResult resultSet = photos.CreateFileQueryWithOptions(option); resultSet.ContentsChanged += resultSet_ContentsChanged; ~~~ 结果集只要发生更改,就会随时触发此事件。然后,应用可以找到最近更改过的一个或多个文件。 从后台跟踪更改涉及的操作稍微多一些。应用可以注册为在设备上的库发生文件更改时收到通知。不支持更复杂的查询或范围,也就是说,应用负责执行少量工作来确保这是其真正关注的更改。 顺便说一句,应用只能注册库更改通知,而不基于文件类型的原因在于索引器的设计方式。根据磁盘上文件的位置筛选查询远远快于基于文件类型执行匹配查询;这降低了我们初始测试中设备的性能。稍后,我还会介绍更多的性能提示,但在这里您需要记住的重要一点是: 与其他类型的筛选操作相比,按文件位置筛选查询结果的速度极快。 我已在博客文章 ([bit.ly/1iPUVIo](http://bit.ly/1iPUVIo)) 中通过代码示例介绍了注册后台任务的步骤,但在这里,让我们一起了解两个更加有趣的步骤。应用必须执行的第一步就是创建后台触发器: ~~~ StorageLibrary library =   await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures); StorageLibraryContentChangedTrigger trigger =   StorageLibraryContentChangedTrigger.Create(library); ~~~ 如果应用会对跟踪多个位置感兴趣,那么您还可以通过库集合创建触发器。在这种情况下,应用只会查询图片库,这是应用的最常见方案之一。您需要确保应用能够正确访问其要尝试更改跟踪的库;否则,在应用尝试创建 StorageLibrary 对象时,系统会返回拒绝访问异常。 在 Windows 移动设备上,这就特别有效,因为系统保证设备上的新图片会在图片库位置下进行编写。无论用户在设置页中选择了什么(通过更改库中包含的文件夹),系统都会这样做。 应用必须使用 Background­ExecutionManager 注册后台任务,并在应用内构建后台任务。当应用位于前台时,可以激活后台任务,因此,任何代码都必须注意文件或注册表访问可能存在的争用条件。 注册完成后,每当注册的库中发生更改时,系统都会调用您的应用。这可能包括您的应用不关注或无法处理的文件。在这种情况下,在后台任务触发时立即应用严格的筛选器是确保不会有浪费的后台处理的最佳方式。 查找最近修改或添加的文件如同针对索引器执行一次查询一样简单。只需请求获取收集时间处于应用关注范围内的所有文件即可。如果需要,还可以在此处使用其他查询适用的相同排序和分组功能。请注意,索引器内部使用祖鲁时间,因此,请务必在使用之前,将所有时间字符串都转换成祖鲁时间。下面展示了如何构建查询: ~~~ QueryOptions options = new QueryOptions(); DateTimeOffset lastSearchTime = DateTimeOffset.UtcNow.AddHours(-1); // This is the conversion to Zulu time, which is used by the indexer string timeFilter = "System.Search.GatherTime:>=" +   lastSearchTime.ToString("yyyy\\-MM\\-dd\\THH\\:mm\\:ss\\Z") options.ApplicationSearchFilter += timeFilter; ~~~ 在此示例中,应用将获取过去一小时内的所有结果。在一些应用中,更明智的做法是,保存上次查询的时间,并将它改用于查询,而所有 DateTimeOffset 都有效。在获得返回的文件列表后,应用可以枚举这些文件(如前所述),也可以使用此列表跟踪新添加的文件。 通过结合使用这两种更改跟踪方法和收集时间,UWP 应用可以跟踪文件系统中的更改,并能轻松地响应磁盘上的更改。这些可能是旧版 Windows 中相对较新的 API,但它们已经用于内置于 Windows 10 的照片、Groove 音乐、OneDrive、Cortana 以及电影和电视应用。知道这些 API 有助于提供绝佳体验后,您大可放心将它们包含在您的应用中。 ## 常规最佳做法 使用索引器的所有应用都应该注意一些事项,以避免出现任何缺陷,并确保应用尽可能快速地运行。这些事项包括:避免在应用的性能关键部分进行任何过于复杂的查询;正确地使用属性枚举;注意索引延迟。 查询的设计方式会对其性能产生重大影响。当索引器针对其支持数据库运行查询时,鉴于信息在磁盘上的分布方式,一些查询的速度比较快。基于文件位置进行筛选的速度始终很快,因为索引器能快速地从查询中消除大量索引部分。这节省了处理器和 I/O 时间,因为在搜索查询词的匹配项时,需要检索和比较的字符串变少了。 索引器的效能强大,可以处理正则表达式,但一些形式的正则表达式以导致速度缓慢而恶名昭著。索引器查询中可以添加的最糟操作就是后缀搜索。这是查询所有以给定值结尾的词语。例如,查询“*tion”会查找其中包含以“tion”结尾的词语的所有文档。 由于索引是按每个令牌中的第一个字母进行排序,因此没有快速方法可以查找此查询的匹配词。必须在整个索引中解码每个令牌,然后将它与搜索词进行比较,这一过程极为缓慢。 枚举可以加快查询速度,但会在国际版本中发生意外行为。任何构建过搜索系统的人都知道,基于枚举进行比较要比执行字符串比较快得多。在索引器中,也同样如此。为了方便您的应用轻松运行,在开始执行费用高昂的字符串比较之前,属性系统会提供大量枚举,以将结果筛选为较少的项目。常见示例是,使用 System.Kind 筛选器将结果限制为应用能处理的几个文件种类,如音乐或文档。 有一个常见错误,使用枚举的所有人都必须注意。如果您的用户只要查找音乐文件,那么英语版本的 Windows 中,向查询添加 System.Kind:=music 可以非常有效地限制搜索结果并加快查询速度。这也对其他一些语言版本有效(甚至可以通过国际化测试),但在系统无法将“music”识别为英文词语,而用本地语言解析此词语的情况下,这样做将无效。 使用枚举(如 System.Kind)的正确方法是,明确指出应用计划将此值用作枚举,而不是搜索词。为此,请使用 enumeration#value 语法。例如,将结果筛选为仅包含音乐文件的正确方法是编写 System.Kind:=System.Kind#Music。这对 Windows 所有语言版本都有效,并会将结果筛选为仅包含可被系统识别为音乐文件的文件。 正确地转义高级查询语法 (AQS) 有助于确保您的用户不必费劲就能重现查询问题。AQS 具有多项功能,可方便用户添加引号或括号来影响查询的处理方式。也就是说,应用必须谨慎转义任何可能包含这些字符的查询词。例如,搜索 Document(8).docx 会导致解析错误,并返回不正确的结果。应用应将搜索词转义为 Document%288%29.docx。这会在索引中返回与搜索词匹配的项,而不会让系统尝试将括号解析为查询的一部分。 有关 AQS 各种功能的最深入探讨,以及如何确保查询正确无误,您可以查看 [bit.ly/1Fhacfl](http://bit.ly/1Fhacfl) 上的文档。文档中包含许多实用信息,更加详细地介绍了本文提及的提示。 关于索引延迟的注意事项: 索引不是即时的。也就是说,根据索引器在索引或通知中显示的项目会比文件编写略有延迟。在正常系统负载下,延迟大约为 100 毫秒,这比大多数应用查询文件系统的速度要快,因此并不会被察觉到。在某些情况下,用户可能在其计算机上迁移数千个文件,这就会导致索引器的速度明显下降。 在这种情况下,建议应用执行以下两项操作: 首先,应在应用最关注的文件系统位置上保持查询未结状态。通常情况,为此,请在应用要搜索的文件系统位置上创建 StorageFileQueryResult 对象。当索引器发现应用具有未结查询时,它会优先在这些范围内创建索引(优先于其他所有范围)。不过,请勿对大于所需的范围这么做。索引器会停止考虑系统回退和用户活动通知,以尽快处理这些更改。因此,用户可能会在这种情况发生时注意到这对系统性能的影响。 另一项建议是提醒用户系统正在进行文件操作。某些应用(如 Cortana)会在其 UI 的顶部显示一条消息,而其他应用则会停止执行复杂查询,并提供简单版本的显示体验。什么最能打造完美的应用体验完全由您自行决定。 ## 总结 本文快速介绍了 Windows 10 中索引器和 Windows 存储 API 的使用者可以使用的功能。若要详细了解如何使用查询传递应用激活上下文,或者如需后台触发器的代码示例,请访问 [bit.ly/1iPUVIo](http://bit.ly/1iPUVIo) 查看团队博客。我们一直在不遗余力地与开发者通力合作,以确保搜索 API 十分方便使用。我们期待您提供反馈意见,告诉我们实用的功能以及希望我们提供的新功能。 * * * Adam Wilson *是 Windows 开发者生态系统和平台团队的项目经理。他负责 Windows 索引器,以及提升推送通知的可靠性。以前,他负责 Windows Phone 8.1 存储 API 方面的工作。您可以通过[adwilso@microsoft.com](mailto:adwilso@microsoft.com) 与他取得联系。*