將對象從存儲中取出來的方法之一是使用NSFetchRequest。但是請注意糜值,一個最常見的錯誤是在你不需要的時候去讀取數(shù)據(jù)个榕。請確保你已經(jīng)閱讀并理解了獲取對象一節(jié)中的內(nèi)容。大多數(shù)時候概耻,遍歷關(guān)系更加有效使套,而使用NSFetchRequest往往成本很高。
通常有兩個原因使用NSFetchRequest來執(zhí)行數(shù)據(jù)獲染媳:(1) 你需要為匹配特定謂詞 (predicate) 的對象搜索整個對象圖侦高;或者 (2) 你想要在比如 table view 這樣的地方顯示所有的對象。其實還有第三種春锋,也是一個較不常見的情況矫膨,就是在遍歷關(guān)系的同時卻想要更高效地預(yù)先獲取數(shù)據(jù)。我們也將簡單深入這個問題期奔。不過我們先來看看兩個主要原因侧馅,它們更加常見并且每個都具有自己的復(fù)雜性。
基礎(chǔ)
在這里我們不會涉及基礎(chǔ)內(nèi)容呐萌,因為一個關(guān)于 Core Data 的名為Fetching Managed Objects的 Xcode 文檔已經(jīng)涵蓋了大量基本原理馁痴。我們將深入到一些更專業(yè)的方面。
搜索對象圖
在我們的交通數(shù)據(jù)的例子中肺孤,我們有 12,800 個車站罗晕,其中有接近 3,000,000 個停留時間相互關(guān)聯(lián)。對接近北緯 52° 29' 57.30"赠堵,東經(jīng) +13° 25' 5.40" 的車站小渊,如果我們想要按照發(fā)車時間介于 8:00 和 8:30 之間的條件來進(jìn)行查找,我們不會想要在這個 context 中加載所有的 12,800 個車站對象和所有三百萬的停留時間對象茫叭,然后再對它們進(jìn)行循環(huán)訪問酬屉。如果我們這樣做,將不得不花費大量時間以及相當(dāng)大的存儲空間以將所有的對象加載到存儲器中。取而代之呐萨,我們想要的是使用 SQLite 來縮減進(jìn)入內(nèi)存的的對象的數(shù)量杀饵。
讓我們從小處開始,為位置接近北緯 52° 29' 57.30" 東經(jīng) +13° 25' 5.40" 的車站創(chuàng)建一個 fetch 請求谬擦。首先我們創(chuàng)建這個 fetch 請求:
我們使用Florian 的 data model 文章中中提到的+entityName方法切距。然后,我們需要將結(jié)果限定為那些接近我們的點的結(jié)果惨远。
我們可以簡單的用一個 (不完全) 正方形區(qū)域圍繞我們的興趣點谜悟。實際在數(shù)學(xué)上這有些復(fù)雜碟摆,因為地球恰好有點類似于一個橢球虑省。但是如果我們假設(shè)地球是球體,則可以得到這個公式:
我們最后可以得到以下內(nèi)容 (均為近似值):
我們的感興趣的點是:
CLLocation *pointOfInterest = [[CLLocation alloc] initWithLatitude:52.4992490
longitude:13.4181670];
我們想在 ±263 英尺(80 米)內(nèi)進(jìn)行搜索:
(當(dāng)我們接近 180° 經(jīng)線的時候徐勃,這個運算不成立羡儿。由于我們的交通數(shù)據(jù)源于離 180° 經(jīng)線很遠(yuǎn)很遠(yuǎn)的柏林礼患,所以我們忽略這個問題。)
指定一種排序描述符毫無意義掠归,因為我們會在內(nèi)存中做第二次遍歷缅叠。不過我們將讓 Core Data 在返回對象里填上所有值。
如果不做這個設(shè)置虏冻,Core Data 將把值取入持久化存儲協(xié)調(diào)器的行緩存 (row cache) 中肤粱,而不是填充實際對象。通常來說這是沒問題的厨相,不過由于我們將立刻訪問所有對象领曼,所以我們并不希望出現(xiàn)這種行為。
編者注:把屬性值先取入緩存中蛮穿,在對象需要的時候再進(jìn)行一次訪問庶骄,這在 Core Data 中是默認(rèn)行為,這種技術(shù)稱為 Faulting践磅。這么做可以避免降低內(nèi)存開銷单刁,但是如果你確定將訪問結(jié)果對象的具體屬性值時,可以禁用 Faults 以提高獲取性能府适。
為安全防范考慮羔飞,最好加上:
執(zhí)行這條 fetch 請求
獲取操作失敗唯一 (可能) 的原因是儲存器損壞(文件被刪除等等),否則就是 fetch 請求中出現(xiàn)了語法錯誤檐春。所以在這里使用NSAssert()是安全的逻淌。
我們現(xiàn)在使用 Core Locations,對內(nèi)存中的數(shù)據(jù)做第二次遍歷疟暖。
和:
至此我們完成了全部設(shè)置恍风。
地理定位性能
使用裝載了 SSD 硬盤的新一代 MacBook Pro 讀取這些數(shù)據(jù)平均約需要 360μs,也就是說誓篱,你每秒可以做大約 2800 次請求朋贬。iPhone 5 平均約需要 1.67ms,每秒 600 次請求窜骄。
如果加上-com.apple.CoreData.SQLDebug1作為啟動參數(shù)傳遞給應(yīng)用程序锦募,我們將得到如下輸出:
除開一些 (對于存儲本身的) 統(tǒng)計信息外,實際為讀取數(shù)據(jù)而生成的 SQL 是:
這正是我們所期望的邻遏。如果我們想要對這個性能進(jìn)行調(diào)查研究糠亩,我們可以使用 SQLEXPLAIN命令。為此准验,我們可以像下面這樣使用命令行sqlite3來打開數(shù)據(jù)庫:
這告訴我們 SQLite 為(ZLONGITUDE>? AND ZLONGITUDEmodel 文章中描述的那樣使用復(fù)合索引則會做的更好赎线。由于我們總是同時搜索經(jīng)度和緯度的組合,這么做會更高效糊饱,而且我們可以去掉經(jīng)度和緯度各自的索引垂寥。
這將使輸出像下面這樣:
在我們的簡單案例中,加上復(fù)合索引幾乎不影響性能另锋。
就像在SQLite 文檔中的說明一樣滞项,如果你的輸出里含有SCAN TABLE的話,你就要提高警惕了夭坪,這基本上意味著 SQLite 需要遍歷所有的記錄來看看那些是相匹配的文判。除非你只存儲了很少的幾個對象,否則你都應(yīng)該使用使用 index室梅。
子查詢
假設(shè)我們只想要那些接近我們的且在接下來 20 分鐘之內(nèi)提供服務(wù)的車站戏仓。
我們可以像這樣為StopTimes的實體創(chuàng)建一個謂詞:
但是如果我們想要的謂詞是可以基于與StopTimes 停留時間對象的關(guān)系而過濾出那些Stop 車站對象,而不是停留時間對象本身的話亡鼠,我們可以使用一個這樣的子查詢:
請注意赏殃,如果接近午夜,這個邏輯是稍有瑕疵的拆宛,因為我們應(yīng)當(dāng)將謂詞一分為二嗓奢。不過該邏輯在這個例子中是可行的。
對于限制數(shù)據(jù)在關(guān)系之上的浑厚,子查詢非常有用股耽。在 Xcode 文檔-[NSExpression expressionForSubquery:usingIteratorVariable:predicate:]中有更多信息。
我們可以簡單的使用and或者&&來組合兩個謂詞钳幅,例如:
或者在代碼中使用+[NSCompoundPredicate andPredicateWithSubpredicates:]物蝙。
我們用一個像這樣的謂詞來作為結(jié)束:
子查詢性能
如果我們看一下生成的 SQL,它會像下面這樣:
這個 fetch 請求在新一代 MacBook Pro 上運行大約需要 12.3 ms敢艰。在 iPhone 5 上诬乞,大約需要 110 ms。請注意,我們有 300 萬 個停留時間 和將近 13,000 個車站震嫉。
explan 這個查詢森瘪,結(jié)果如下:
請注意,我們?nèi)绾螌χ^詞排序非常重要票堵。我們希望把經(jīng)緯度放在前面扼睬,因為代價低,而子查詢由于代價高則放在語句最后悴势。
文本搜索
搜索文本是一種常見的情況窗宇。在我們的例子中,來看看使用名稱來搜索車站實體特纤。
柏林有個被稱為 "U G?rlitzer Bahnhof (Berlin)" 的車站军俊。一種很傻很天真的搜索該站的方法如下:
如果你想按照如下所示做的話,事情會變得更糟 (比如進(jìn)行一項大小寫和(或)音調(diào)不敏感的查詢捧存。):
事實上粪躬,事情并不是那么簡單。Unicode 非常復(fù)雜矗蕊,并且有很多陷阱短蜕。首要的是很多字符可以通過多種方式來表示。U+00F6和U+006F都代表U+0308都可以表示 "?."傻咖。如果你身處 ASCII 碼的世界之外時朋魔,像大寫 / 小寫這樣的概念就會非常復(fù)雜。
SQLite 會為你減輕負(fù)擔(dān)卿操,但它是要付出代價的警检。雖然它看起來很直接,但事實并非如此害淤。對于字符串搜索扇雕,我們想做的是在我們有一個規(guī)范化的版本可以在其中進(jìn)行搜索。我們將消除音調(diào)符號窥摄,把字符串變成小寫字母镶奉,然后將其放入一個normalizedName字段中。然后我們將對用于搜索的字符串做同樣的事情崭放。然后 SQLite 就不必考慮音調(diào)和大小寫哨苛,在大小寫和音調(diào)不敏感的情況下,搜索就仍會很快币砂。但是我們必須先完成一系列繁重的任務(wù)建峭。
在新一代 MacBook Pro 上,使用示例代碼使用BEGINSWITH[cd]和示例的字符串搜索需要 7.6ms (130 次搜索 / 秒)决摧,在 iPhone 5 上這個數(shù)字是每次搜索 47ms亿蒸,每秒進(jìn)行 21 次搜索凑兰。
為了將字符串轉(zhuǎn)換為小寫并移除其音調(diào),我們可以使用CFStringTransform():
我們將更新Stop類來自動更新normalizedName:
有了這些边锁,我們就可以用BEGINSWITH代替BEGINSWITH[cd]來搜索了:
在新一代 MacBook Pro 上姑食,使用示例代碼中的示例字符串搜索BEGINSWITH需要 6.2ms(160 次搜索 / 秒),在 iPhone 5 大約上需要 40ms砚蓬,25 次搜索 / 秒矢门。
自由文本搜索
我們的搜索還只能在字符串的開頭和搜索字符串相匹配的情況下有效。要解決這個問題就要創(chuàng)建另一個用來搜索的實體灰蛙。我們稱這個實體為SearchTerm,給其一個normalizedWord屬性隔躲,以及一個和Stop的關(guān)系摩梧。對于每個車站我們將規(guī)范它們的名稱,并將其拆分成一個個詞宣旱。例如:
對于每個詞仅父。我們創(chuàng)建一個SearchTerm和一個從Stop到它的所有SearchTerm對象的關(guān)系。當(dāng)用戶輸入一個字符串浑吟,我們用以下代碼在SearchTerm對象的normalizedWord上搜索:
這也可以在Stop對象中直接用子查詢完成笙纤。
獲取所有對象
如果我們的獲取請求中沒有設(shè)置謂詞,我們將為獲取到給定實體的所有對象组力。如果我們對StopTimes實體這樣做的話省容,我們將會牽涉 300 萬個對象。這將會變得緩慢燎字,以及占用大量內(nèi)存腥椒。然而有時候,我們就是需要獲取所有對象候衍。常見的例子是我們想要在一個 table view 中顯示所有對象笼蛛。
在這種情況中,我們要做的是設(shè)置批處理量:
request.fetchBatchSize = 50;
當(dāng)我們設(shè)置了批處理量運行-[NSManagedObjectContext executeFetchRequest:error:]的時候蛉鹿,我們?nèi)匀粫玫揭粋€返回的數(shù)組滨砍。我們可以查詢它的元素數(shù)量(對于StopTimes實體而言,這將接近 300 萬)妖异,不過 Core Data 將只會隨著我們對數(shù)組的循環(huán)訪問將對象填充進(jìn)去惋戏。如果這些對象不再被訪問,Core Data 則會再次清理對象随闺。簡單來說日川,數(shù)組的批處理量為 50(在這個例子中)。Core Data 將一次獲取 50 個對象矩乐。一旦有超過一定數(shù)量的批量對象龄句,Core Data 將釋放最舊一批對象回论。于是,你就可以在這樣的數(shù)組中循環(huán)訪問所有對象分歇,而無需在存儲器中同時存所有 300 萬個對象傀蓉。
在 iOS 中,如果你使用NSFetchedResultsController且有很多對象职抡,請確保你的 fetch 請求中設(shè)置了fetchBatchSize葬燎。你需要實際實驗以確定多少的處理量更適合你。一般來說缚甩,將其設(shè)置為你要顯示的數(shù)目的兩倍谱净,會是一個不錯的開始。