緩存
在眾多可以本地保存數(shù)據(jù)的技術(shù)中,有三種脫穎而出:URL緩存、數(shù)據(jù)模型緩存(利用NSKeyedArchiver)和Core Data。
假設(shè)你正在開發(fā)一個應(yīng)用,需要緩存數(shù)據(jù)以改善應(yīng)用表現(xiàn)出的性能,你應(yīng)該實現(xiàn)按需緩存(使用數(shù)據(jù)模型緩存或URL緩存)。另一方面,如果需要數(shù)據(jù)能夠離線訪問,而且具有合理的存儲方式以便離線編輯,那么就用高級序列化技術(shù)(如Core Data)。
那么緩存策略大致分為兩種:按需緩存和預(yù)緩存。
按需緩存是指把從服務(wù)器獲取的內(nèi)容已某種格式放在本地的文件系統(tǒng),之后對于每次請求,檢查緩存中時候存在這塊數(shù)據(jù),只有當(dāng)數(shù)據(jù)不存在(或過期)的情況才從服務(wù)器獲取。這樣的話,緩存層就和處理器的高速緩存差不多。獲取數(shù)據(jù)的速度比數(shù)據(jù)本身重要。按需緩存的工作原理類似于瀏覽器緩存,它允許我們查看以前產(chǎn)看或者訪問過的內(nèi)容。按需緩存可以通過在打開一個viewcontroller是按需地緩存數(shù)據(jù)模型來實現(xiàn),而不是在一個后臺線程上做這件事,已可以在一個URL請求返回成功應(yīng)答時實現(xiàn)按需緩存。
預(yù)緩存是指把內(nèi)容放在本地以備將來訪問,對預(yù)緩存來說,數(shù)據(jù)丟失或不命中是不可接受的,比如說用戶下載了文章準(zhǔn)備在地鐵上看,卻發(fā)現(xiàn)設(shè)備商不存在這些文章。ps:實現(xiàn)預(yù)緩存可能需要一個后臺線程訪問數(shù)據(jù)并以有意義的格式保存,以便本地緩存無需重新連接服務(wù)器即可編輯。
選擇使用按需緩存還是預(yù)緩存的一個簡便方法是判斷是否需要在下載數(shù)據(jù)之后處理數(shù)據(jù)。后期處理數(shù)據(jù)可能是以用戶產(chǎn)生編輯的形式,也可能是更新下載的數(shù)據(jù),比如重寫HTML頁面里的圖片鏈接以指向本地緩存圖片。如果一個應(yīng)用需要做上面提到的任何后期處理,就必須實現(xiàn)預(yù)緩存。
一,按需緩存:數(shù)據(jù)模型緩存與URL緩存
按需緩存可以用數(shù)據(jù)模型緩存或URL緩存來實現(xiàn)。兩種方式各有優(yōu)缺點,要使用哪一種取決于服務(wù)器的實現(xiàn)。URL緩存的實現(xiàn)原理和瀏覽器緩存或代理服務(wù)器緩存類似。當(dāng)服務(wù)器設(shè)計得體,遵循HTTP 1.1的緩存規(guī)范時,這種緩存效果最好。如果服務(wù)器是SOAP服務(wù)器(或者實現(xiàn)類似于RPC服務(wù)器或RESTful服務(wù)器),就需要用數(shù)據(jù)模型緩存。如果服務(wù)器遵循HTTP 1.1緩存規(guī)范,就用URL緩存。數(shù)據(jù)模型緩存允許客戶端(iOS應(yīng)用)掌控緩存失效的情形,當(dāng)開發(fā)者實現(xiàn)URL緩存時,服務(wù)器通過HTTP 1.1的緩存控制頭控制緩存失效。盡管有些程序員覺得這種方式違反直覺,而且實現(xiàn)起來也很復(fù)雜(尤其是在服務(wù)器端),但這可能是實現(xiàn)緩存的好辦法。事實上,MKNetworkKit提供了對HTTP 1.1緩存標(biāo)準(zhǔn)的原生支持。
數(shù)據(jù)模型緩存
按需緩存是在視圖從視圖層次結(jié)構(gòu)中消失時做的(從技術(shù)上講,是在viewWillDisappear:方法中)。
在viewWillAppear方法中,查看緩存中是否有顯示這個視圖所需的數(shù)據(jù)。如果有就獲取數(shù)據(jù),再用緩存數(shù)據(jù)更新用戶界面。然后檢查緩存中的數(shù)據(jù)是否已經(jīng)過期。你的業(yè)務(wù)規(guī)則應(yīng)該能夠確定什么是新數(shù)據(jù)、什么是舊數(shù)據(jù)。如果內(nèi)容是舊的,把數(shù)據(jù)顯示在UI上,同時在后臺從服務(wù)器獲取數(shù)據(jù)并再次更新UI。如果緩存中沒有數(shù)據(jù),顯示一個轉(zhuǎn)動的圓圈表示正在加載,同時從服務(wù)器獲取數(shù)據(jù)。得到數(shù)據(jù)后,更新UI。
視圖控制器的viewWillAppear:方法中從緩存恢復(fù)數(shù)據(jù)模型對象的代碼片段
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
? ? NSUserDomainMask, YES);
NSString *cachesDirectory = [paths objectAtIndex:0];
NSString *archivePath = [cachesDirectory
? ? stringByAppendingPathComponent:@"AppCache/MenuItems.archive"];
NSMutableArray *cachedItems = [NSKeyedUnarchiver
? ? unarchiveObjectWithFile:archivePath];if(cachedItems == nil)
? self.menuItems = [AppDelegate.engine localMenuItems];else
? self.menuItems = cachedItems;
NSTimeInterval stalenessLevel = [[[[NSFileManager defaultManager]
? ? attributesOfItemAtPath:archivePath error:nil]
fileModificationDate] timeIntervalSinceNow];if(stalenessLevel > THRESHOLD)
? self.menuItems = [AppDelegate.engine localMenuItems];
[self updateUI];
緩存機制的邏輯流如下所示:
1、視圖控制器在歸檔文件MenuItems.archive中檢查之前緩存的項并反歸檔。
2、如果MenuItems.archive不存在,視圖控制器調(diào)用方法從服務(wù)器獲取數(shù)據(jù)。
3、如果MenuItems.archive存在,視圖控制器檢查歸檔文件的修改時間以確認(rèn)緩存數(shù)據(jù)有多舊。如果數(shù)據(jù)過期了(由業(yè)務(wù)需求決定),再從服務(wù)器獲取一次數(shù)據(jù)。否則顯示緩存的數(shù)據(jù)。
接下來,把下面的代碼加入viewDidDisappear方法可以把模型(以NSKeyedArchiver的形式)保存在Library/Caches目錄中。
視圖控制器的viewWillDisappear:方法中緩存數(shù)據(jù)模型的代碼片段
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
? NSUserDomainMask, YES);
NSString *cachesDirectory = [paths objectAtIndex:0];
NSString *archivePath = [cachesDirectory stringByAppendingPathComponent:@"? ? AppCache/MenuItems.archive"];
[NSKeyedArchiver archiveRootObject:self.menuItems toFile:archivePath];
視圖消失時要把menuItems數(shù)組的內(nèi)容保存在歸檔文件中。注意,如果不是在viewWillAppear:方法中從服務(wù)器獲取數(shù)據(jù)的話,這種情況不能緩存。
所以,只需在視圖控制器中加入不到10行的代碼(并將Accessorizer生成的幾行代碼加入模型),就可以為應(yīng)用添加緩存支持了。
重構(gòu)
當(dāng)開發(fā)者有多個視圖控制器時,前面的代碼可能會有冗余。我們可以通過抽象出公共代碼并移入名為AppCache的新類來避免冗余。AppCache是處理緩存的應(yīng)用的核心。把公共代碼抽象出來放入AppCache可以避免viewWillAppear:和viewWillDisappear:中出現(xiàn)冗余代碼。
重構(gòu)這部分代碼,使得視圖控制器的viewWillAppear/viewWillDisappear代碼塊看起來如下所示。加粗部分顯示重構(gòu)時所做的修改,我會在代碼后面解釋。
視圖控制器的viewWillAppear:方法中用AppCache類緩存數(shù)據(jù)模型的重構(gòu)代碼片段(MenuItemsViewController.m)
-(void) viewWillAppear:(BOOL)animated {
? self.menuItems = [AppCache getCachedMenuItems];
? [self.tableView reloadData];if([AppCache isMenuItemsStale] || !self.menuItems) {
? ? [AppDelegate.engine fetchMenuItemsOnSucceeded:^(NSMutableArray? ? ? ? *listOfModelBaseObjects) {
? ? self.menuItems = listOfModelBaseObjects;
? ? [self.tableView reloadData];
? } onError:^(NSError *engineError) {
? ? [UIAlertView showWithError:engineError];
? }];
}
? [super viewWillAppear:animated];
}? ? -(void) viewWillDisappear:(BOOL)animated {
? [AppCache cacheMenuItems:self.menuItems];
? [super viewWillDisappear:animated];
}
AppCache類把判斷數(shù)據(jù)是否過期的邏輯從視圖控制器中抽象出來了,還把緩存保存的位置也抽象出來了。稍后在本章中我們還會修改AppCache,再引入一層緩存,內(nèi)容會保存在內(nèi)存中。
因為AppCache抽象出了緩存的保存位置,我們就不需要為復(fù)制粘貼代碼來獲得應(yīng)用的緩存目錄而操心了。如果應(yīng)用類似于iHotelApp,開發(fā)者可通過為每個用戶創(chuàng)建子目錄即可輕松增強緩存數(shù)據(jù)的安全性。然后我們就可以修改AppCache中的輔助方法,現(xiàn)在它返回的是緩存目錄,我們可以讓它返回當(dāng)前登錄用戶的子目錄。這樣,一個用戶緩存的數(shù)據(jù)就不會被隨后登錄的用戶看到了。
因為緩存數(shù)據(jù)不是由用戶產(chǎn)生的,所以緩存數(shù)據(jù)應(yīng)該保存在NSCachesDirectory,而不是NSDocumentsDirectory。以下代碼在Library/caches文件夾下創(chuàng)建名為MyAppCache的目錄。
NSArray*paths =NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES);
NSString*cachesDirectory = [pathsfirstObject];
cachesDirectory = [cachesDirectorystringByAppendingPathComponent:@"MyAppCache"];
1.實現(xiàn)數(shù)據(jù)模型緩存
可以用NSKeyedArchiver類來實現(xiàn)數(shù)據(jù)緩存模型,前提是模型類遵循NSCoding協(xié)議。
當(dāng)模型遵循NSCoding協(xié)議時,只要調(diào)用以下其中的一個方法即可歸檔對象
[NSKeyedArchiver archiveRootObject:<#(nonnull id)#> toFile:<#(nonnull NSString *)#>];
[NSKeyedArchiver archivedDataWithRootObject:<#(nonnull id)#>];
第一個方法在archiveFilePath指定的路徑下創(chuàng)建一個歸檔文件。第二個方法返回一個NSData對象。NSData通常更快,因為沒有文件訪問開銷,但對象保存在應(yīng)用的內(nèi)存中,如果不定期檢查的話會很快用完內(nèi)存。
NSKeyedUnarchiver類用于從文件(或者NSData指針)反歸檔模型。根據(jù)反歸檔的位置,選擇使用下面兩個類方法。
[NSKeyedUnarchiver unarchiveObjectWithData:data];
[NSKeyedUnarchiver unarchiveObjectWithFile:archiveFilePath];
接下來我們著重關(guān)注按需緩存的實現(xiàn)。
按需緩存是從視圖從視圖層次結(jié)構(gòu)中消失的時候做的,從技術(shù)上講是viewwillappear:方法中
以下是實現(xiàn)按需緩存的viewcontroller的控制流

自定義一個Person類,并實現(xiàn)NSCoding協(xié)議



緩存失效和版本控制問題從viewcontroller中抽象出來,接下來我們?yōu)锳ppCache創(chuàng)建內(nèi)存緩存。
3.內(nèi)存緩存
接下來將介紹如何給AppCache類添加一層透明的,位于內(nèi)存中的緩存,并且設(shè)計一個LRU(Least Recently Used)算法吧緩存的數(shù)據(jù)保存到磁盤。
以下簡單地列出創(chuàng)建內(nèi)存緩存的步驟。
(1)添加變量來存放內(nèi)存緩存數(shù)據(jù)
(2)限制內(nèi)存緩存大小,并且把最近最少使用的項寫入文件。
(3)處理內(nèi)存警告,并把內(nèi)存緩存以文件形式寫入閃存。
(4)當(dāng)應(yīng)用關(guān)閉、退出、進(jìn)入后臺,把內(nèi)存緩存全部以文件形式寫入閃存。
3.1為AppCache設(shè)計內(nèi)存緩存
AppCache中的變量

將模型對象透明地保存到內(nèi)存緩存中。

上面的代碼調(diào)用了一個輔助方法,cacheData:toFile:,而不是直接寫入文件。這個方法會把從NSKeyedArchiver得到的NSData保存到內(nèi)存緩存中。當(dāng)內(nèi)存緩存達(dá)到預(yù)定的內(nèi)存限制時,它會檢查并刪除最近最少使用的數(shù)據(jù),然后把數(shù)據(jù)保存到文件中。

3.2處理內(nèi)存警告
在靜態(tài)初始化方法中,向通知中心添加觀察者。

然后寫一個方法來把內(nèi)存中的項保存到文件:

3.3處理結(jié)束和進(jìn)入后臺通知

最后記得在dealloc移除觀察者!
緩存版本控制:
我們寫的AppCache類從視圖控制器中抽象出了按需緩存。當(dāng)視圖出現(xiàn)和消失時,緩存就在幕后工作。然而,當(dāng)你更新應(yīng)用時,模型類可能會發(fā)生變化,這意味著之前歸檔的任何數(shù)據(jù)將不能恢復(fù)到新的模型上。正如之前所講,對按需緩存來說,數(shù)據(jù)并沒有那么重要,開發(fā)者可以刪除數(shù)據(jù)并更新應(yīng)用。
二,實預(yù)緩存
實現(xiàn)預(yù)緩存可能需要一個后臺線程訪問數(shù)據(jù)并以有意義的格式保存,以便本地緩存無需重新連接服務(wù)器即可被編輯。編輯可能是“標(biāo)記記錄為已讀”或“加入收藏”,或其他類似的操作。這里**有意義的格式**是指可以用這種方式保存內(nèi)容,不用和服務(wù)器通信就可以在本地作出上面提到的修改,并且一旦再次連上網(wǎng)就可以把變更發(fā)送回服務(wù)器。這種能力和Foursquare等應(yīng)用不同,雖然使用后者你能在無網(wǎng)絡(luò)連接的情況下看到自己是哪些地點的地主(Mayor),當(dāng)然前提是進(jìn)行了緩存,但無法成為某個地點的地主。Core Data(或者任何結(jié)構(gòu)化存儲)是實現(xiàn)這種緩存的一種方式。
下一節(jié)會解釋預(yù)緩存策略。我們剛才已經(jīng)了解到預(yù)緩存需要用到更結(jié)構(gòu)化的數(shù)據(jù)格式,接下來看看Core Data和SQLite。
Core Data:
正如Marcus Zarra所說,Core Data更像是一個對象序列化框架,而不僅僅是一個數(shù)據(jù)庫API:
大家誤認(rèn)為Core
Data是一個Cocoa的數(shù)據(jù)庫API……其實它是個可以持久化到磁盤的對象框架(Zarra,2009年)。
要深入理解Core Data,看一下Marcus S. Zarra寫的*Core Data: Apple's API for Persisting Data on Mac OS X*(Pragmatic Bookshelf, 2009. ISBN 9781934356326)。
要在Core Data中保存數(shù)據(jù),首先創(chuàng)建一個Core Data模型文件,并創(chuàng)建實體(Entity)和關(guān)系(Relationship);然后寫好保存和獲取數(shù)據(jù)的方法。應(yīng)用可以借助Core Data獲取真正的離線訪問功能,就像蘋果內(nèi)置的Mail和Calendar應(yīng)用一樣。實現(xiàn)預(yù)緩存時必須定期刪除不再需要的(過時的)數(shù)據(jù),否則緩存會不斷增長并影響應(yīng)用的性能。同步本地變更是通過追蹤變更集并發(fā)送回服務(wù)器實現(xiàn)的。變更集的追蹤有很多算法,我推薦的是Git版本控制系統(tǒng)所用的(此處沒有涉及如何與遠(yuǎn)程服務(wù)器同步緩存,這不在本書討論范圍之內(nèi))。
1. 用Core Data實現(xiàn)按需緩存
盡管從技術(shù)上講可以用Core Data來實現(xiàn)按需緩存,但我不建議這么做。Core Data的優(yōu)勢是不用反歸檔完整的數(shù)據(jù)就可以獨立訪問模型的屬性。然而,在應(yīng)用中實現(xiàn)Core Data帶來的復(fù)雜度抵消了優(yōu)勢。此外,對于按需緩存實現(xiàn)來說,我們可能并不需要獨立訪問模型的屬性。
2. 原始的SQLite
可以通過鏈接libsqlite3的庫來把SQLite嵌入應(yīng)用,但是這么做有很大的缺陷。所有的sqlite3庫和對象關(guān)系映射(Object Relational Mapping,ORM)機制幾乎總是會比Core Data慢。此外,盡管sqlite3本身是線程安全的,但是iOS上的二進(jìn)制包則不是。所以除非用定制編譯的sqlite3庫(用線程安全的編譯參數(shù)編譯),否則開發(fā)者就有責(zé)任確保從sqlite3讀取數(shù)據(jù)或者往sqlite3寫入數(shù)據(jù)是線程安全的。Core Data有這么多特性而且內(nèi)置線程安全,所以我建議在iOS中盡量避免使用SQLite。
唯一應(yīng)該在iOS應(yīng)用中用原始的SQLite而不用Core Data的例外情況是,資源包中有應(yīng)用程序相關(guān)的數(shù)據(jù)需要在所有應(yīng)用支持的第三方平臺上共享,比如說運行在iPhone、Android、BlackBerry和Windows Phone上的某個應(yīng)用的位置數(shù)據(jù)庫。不過這也不是緩存了。
iOS內(nèi)存緩存:
目前為止,所有iOS設(shè)備都帶有閃存,而閃存有點小問題:它的讀寫壽命是有限的。盡管這個壽命跟設(shè)備的使用壽命比起來很長,但是仍然需要避免過于頻繁地讀寫閃存。在上一個例子中,視圖隱藏時是直接緩存到磁盤的,而視圖顯示時又是直接從磁盤讀取的。這種行為會使用戶設(shè)備的緩存負(fù)擔(dān)很重。為避免這個問題,我們可以再引入一層緩存,利用設(shè)備的RAM而不是閃存(用NSMutableDictionary)。在24.2.1節(jié)的“實現(xiàn)數(shù)據(jù)模型緩存”中,我們介紹了創(chuàng)建歸檔的兩種方法:一個是保存到文件,另一個是保存為NSData對象。這次會用到第二個方法,我們會得到一個NSData指針,將該指針保存到NSMutableDictionary中,而不是文件系統(tǒng)里的平面文件。引入內(nèi)存緩存的另一個好處是,在歸檔和反歸檔內(nèi)容時性能會略有提升。聽起來很復(fù)雜,實際上并不復(fù)雜。本節(jié)將介紹如何給AppCache類添加一層透明的、位于內(nèi)存中的緩存。(“透明”是指調(diào)用代碼,即視圖控制器,甚至不知道這層緩存的存在,而且也不需要改動任何代碼。)我們還會設(shè)計一個LRU(Least Recently Used,最近最少使用)算法來把緩存的數(shù)據(jù)保存到磁盤。
以下簡單列出了要創(chuàng)建內(nèi)存緩存需要的步驟。這些步驟將會在下面幾節(jié)中詳細(xì)解釋。
1、添加變量來存放內(nèi)存緩存數(shù)據(jù)。
2、限制內(nèi)存緩存大小,并且把最近最少使用的項寫入文件,然后從內(nèi)存緩存中刪除。RAM是有限的,達(dá)到使用極限就會觸發(fā)內(nèi)存警告。收到警告時不釋放內(nèi)存會使應(yīng)用崩潰。我們當(dāng)然不希望發(fā)生這種事,所以要為內(nèi)存緩存設(shè)置一個最大閾值。當(dāng)緩存滿了以后再添加任何東西時,最近最少使用的對象應(yīng)該被保存到文件(閃存中)。
3、處理內(nèi)存警告,并把內(nèi)存緩存以文件形式寫入閃存。
4、當(dāng)應(yīng)用關(guān)閉、退出,或進(jìn)入后臺時,把內(nèi)存緩存全部以文件形式寫入閃存。