# 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)?與他取得聯系。*
- 介紹
- Essential .NET - C# 異常處理
- 崛起 - 具備批判精神
- Windows 10 - 通過搜索索引器加快文件操作速度
- 最前沿的技術 - 利用用戶體驗驅動設計改善體系結構
- 異步編程 - 從頭開始執行異步操作
- 數據點 - Aurelia 與 DocumentDB 結合: 結合之旅
- ASP.NET - 將 ASP.NET 用作高性能文件下載器
- 測試運行 - 使用 C# 執行 t-檢驗
- Microsoft Azure - 通過 SonarQube 和 TFS 管理技術債務
- 孜孜不倦的程序員 - 如何成為 MEAN: Express 路由
- 別讓我打開話匣子 - Alan Turing 和 Ashley Madison
- 編輯寄語 - 歡迎使用 Essential .NET