標籤:
- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
- (void)removeAllCachedResponses;
為什麼要有緩衝
應用需要離線工作的主要原因就是改善應用所表現出的效能。將應用內容緩衝起來就可以支援離線。我們可以用兩種不同的緩衝來使應用離線工作。第一種是**按需緩衝**,這種情況下應用緩衝起請求應答,就和Web瀏覽器的工作原理一樣;第二種是**預緩衝**,這種情況是緩衝全部內容(或者最近n條記錄)以便離線訪問。
像第14章中開發的Web服務應用利用按需緩衝技術來改善可感知的效能而不是提供離線訪問。離線訪問只是無心插柳的結果。Twitter和Foursquare就是很好的例子。這類應用得到的資料通常很快就會過時。對於一條幾天前的推文或者朋友上周在哪裡你能有多大興趣?一般來說,一條推文或者一條簽到的資訊只在幾個小時內有意義,而24小時之後就變得無關緊要。不過大部分Twitter用戶端還是會緩衝推文,而Foursquare的官方用戶端在無網路連接的情況下開啟,會顯示上次的狀態。
大家可以用自己喜歡的Twitter用戶端來試一下,Twitter for iPhone、Tweetbot或其他應用:開啟某個朋友的設定檔並瀏覽他的時間軸。應用會擷取時間軸並填充頁面。載入時間軸時會看到一個表示正在載入的圓圈在旋轉。現在進入另一個頁面,然後再回來開啟時間軸。你會發現這次是瞬間載入的。應用還是在後台重新整理內容(在上次開啟的基礎上),但是它會顯示上次緩衝的內容而不是無趣地轉圈,這樣看起來就快多了。如果沒有緩衝,使用者每次開啟一個頁面都會看到圓圈在旋轉。無論網路連接快還是慢,減小網路載入慢的影響,讓它看起來很快,是iOS開發人員的責任。這就能大大改善使用者滿意度,從而提高了應用在App Store中的評分。
另一種緩衝更加重視被快取資料,並且能快速編輯被緩衝的記錄而無需串連到伺服器。代表應用程式套件括Google Reader用戶端,稍後閱讀類的應用Instapaper等。
緩衝的策略:
上一節中討論到按需緩衝和預緩衝,它們在設計和實現上有很大的不同。按需緩衝是指把從伺服器擷取的內容以某種格式存放在本地檔案系統,之後對於每次請求,檢查緩衝中是否存在這塊資料,只有當資料不存在(或者到期)的情況下才從伺服器擷取。這樣的話,緩衝層就和處理器的快取差不多。擷取資料的速度比資料本身重要。而預緩衝是把內容放在本地以備將來訪問。對預緩衝來說,資料丟失或者緩衝不命中是不可接受的,比方使用者下載了文章準備在地鐵上看,但卻發現裝置上不存在這些文章。
像Twitter、Facebook和Foursquare這樣的應用屬於按需緩衝,而Instapaper和Google Reader等用戶端則屬於預緩衝。
實現預緩衝可能需要一個後台線程訪問資料並以有意義的格式儲存,以便本機快取無需重新串連伺服器即可被編輯。編輯可能是“標記記錄為已讀”或“加入收藏”,或其他類似的操作。這裡**有意義的格式**是指可以用這種方式儲存內容,不用和伺服器通訊就可以在本地作出上面提到的修改,並且一旦再次連上網就可以把變更發送回伺服器。這種能力和Foursquare等應用不同,雖然使用後者你能在無網路連接的情況下看到自己是哪些地點的地主(Mayor),當然前提是進行了緩衝,但無法成為某個地點的地主。Core Data(或者任何結構化儲存)是實現這種緩衝的一種方式。
按需緩衝工作原理類似於瀏覽器緩衝。它允許我們查看以前查看或者訪問過的內容。按需緩衝可以通過在開啟一個視圖控制器時按需地快取資料模型(建立一個資料模型緩衝)來實現,而不是在一個後台線程上做這件事。也可以在一個URL請求返回成功(200 OK)應答時實現按需緩衝(建立一個URL緩衝)。兩種方法各有利弊,稍後我會在24.3節和24.6節中解釋各個方法的優缺點。
選擇使用按需緩衝還是預緩衝的一個簡便方法是判斷是否需要在下載資料之後處理資料。後期處理資料可能是以使用者產生編輯的形式,也可能是更新下載的資料,比如重寫HTML頁面裡的圖片連結以指向本機快取圖片。如果一個應用需要做上面提到的任何後期處理,就必須實現預緩衝。
儲存緩衝:
第三方應用只能把資訊儲存在應用程式的沙箱中。因為快取資料不是使用者產生的,所以它應該被儲存在NSCachesDirectory,而不是NSDocumentsDirectory。為快取資料建立獨立目錄是一項不錯的實踐。在下面的例子中,我們將在Library/caches檔案夾下建立名為MyAppCache的目錄。可以這樣建立:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); NSString *cachesDirectory = [paths objectAtIndex:0]; cachesDirectory = [cachesDirectory stringByAppendingPathComponent:@"MyAppCache"];
把緩衝儲存在快取檔案夾下的原因是iCloud(和iTunes)的備份不包括此目錄。如果在Documents目錄下建立了大尺寸的快取檔案,它們會在備份的時候被上傳到iCloud並且很快就用完有限的空間(寫作本書時大約為5 GB)。你不會這麼乾的——誰不想成為使用者iPhone上的良民?NSCachesDirectory正是解決這個問題的。
預緩衝是用進階資料庫(比如原始的SQLite)或者對象序列化架構(比如Core Data)實現的。我們需要根據需求認真選擇不同的技術。本節第5點“應該用哪種緩衝技術”給出了一些建議:什麼時候該用URL緩衝或者資料模型緩衝,而什麼時候又該用Core Data。接下來先看一下資料模型緩衝的實現細節。
1. 實現資料模型緩衝
可以用NSKeyedArchiver類來實現資料模型緩衝。為了把模型對象用NSKeyedArchiver歸檔,模型類需要遵循NSCoding協議。
NSCoding協議方法
- (void)encodeWithCoder:(NSCoder *)aCoder; - (id)initWithCoder:(NSCoder *)aDecoder;
當模型遵循NSCoding協議時,歸檔對象就很簡單,只要調用下列方法中的一個:
[NSKeyedArchiver archiveRootObject:objectForArchiving toFile:archiveFilePath]; [NSKeyedArchiver archivedDataWithRootObject:objectForArchiving];
第一個方法在archiveFilePath指定的路徑下建立一個歸檔檔案。第二個方法則返回一個NSData對象。NSData通常更快,因為沒有檔案訪問開銷,但對象儲存在應用的記憶體中,如果不定期檢查的話會很快用完記憶體。在iPhone上定期緩衝到快閃記憶體的功能也是不明智的,因為跟硬碟不同,快閃記憶體讀寫壽命是有限的。開發人員得儘可能平衡好兩者的關係。24.3節會詳細介紹歸檔實現緩衝。
NSKeyedUnarchiver類用於從檔案(或者NSData指標)反歸檔模型。根據反歸檔的位置,選擇使用下面兩個類方法。
[NSKeyedUnarchiver unarchiveObjectWithData:data]; [NSKeyedUnarchiver unarchiveObjectWithFile:archiveFilePath];
這四個方法在轉化序列化資料時能派上用場。
使用任何NSKeyedArchiver/NSKeyedUnarchiver的前提是模型實現了NSCoding協議。不過要做到這一點很容易,可以用Accessorizer類工具自動實現NSCoding協議。(24.8節列出了Accessorizer在Mac App Store中的連結。)
下一節會解釋預緩衝策略。我們剛才已經瞭解到預緩衝需要用到更結構化的資料格式,接下來看看Core Data和SQLite。
2. Core Data
正如Marcus Zarra所說,Core Data更像是一個對象序列化架構,而不僅僅是一個資料庫API:
大家誤認為Core
Data是一個Cocoa的資料庫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中儲存資料,首先建立一個Core Data模型檔案,並建立實體(Entity)和關係(Relationship);然後寫好儲存和擷取資料的方法。應用可以藉助Core Data擷取真正的離線訪問功能,就像蘋果內建的Mail和Calendar應用一樣。實現預緩衝時必須定期刪除不再需要的(過時的)資料,否則緩衝會不斷增長並影響應用的效能。同步本地變更是通過追蹤變更集並發送回伺服器實現的。變更集的追蹤有很多演算法,我推薦的是Git版本控制系統所用的(此處沒有涉及如何與遠程伺服器同步緩衝,這不在本書討論範圍之內)。
3. 用Core Data實現按需緩衝
儘管從技術上講可以用Core Data來實現按需緩衝,但我不建議這麼做。Core Data的優勢是不用反歸檔完整的資料就可以獨立訪問模型的屬性。然而,在應用中實現Core Data帶來的複雜度抵消了優勢。此外,對於按需緩衝實現來說,我們可能並不需要獨立訪問模型的屬性。
4. 原始的SQLite
可以通過連結libsqlite3的庫來把SQLite嵌入應用,但是這麼做有很大的缺陷。所有的sqlite3庫和對象關係映射(Object Relational Mapping,ORM)機制幾乎總是會比Core Data慢。此外,儘管sqlite3本身是安全執行緒的,但是iOS上的二進位包則不是。所以除非用定製編譯的sqlite3庫(用安全執行緒的編譯參數編譯),否則開發人員就有責任確保從sqlite3讀取資料或者往sqlite3寫入資料是安全執行緒的。Core Data有這麼多特性而且內建安全執行緒,所以我建議在iOS中盡量避免使用SQLite。
唯一應該在iOS應用中用原始的SQLite而不用Core Data的例外情況是,資源套件中有應用程式相關的資料需要在所有應用支援的第三方平台上共用,比如說運行在iPhone、Android、BlackBerry和Windows Phone上的某個應用的位置資料庫。不過這也不是緩衝了。
5. 應該用哪種緩衝技術
在眾多可以本地儲存資料的技術中,有三種脫穎而出:URL緩衝、資料模型緩衝(利用NSKeyedArchiver)和Core Data。
假設你正在開發一個應用,需要快取資料以改善應用表現出的效能,你應該實現按需緩衝(使用資料模型緩衝或URL緩衝)。另一方面,如果需要資料能夠離線訪問,而且具有合理的儲存方式以便離線編輯,那麼就用進階序列化技術(如Core Data)。
6. 資料模型緩衝與URL緩衝
按需緩衝可以用資料模型緩衝或URL緩衝來實現。兩種方式各有優缺點,要使用哪一種取決於伺服器的實現。URL緩衝的實現原理和瀏覽器緩衝或Proxy 伺服器緩衝類似。當伺服器設計得體,遵循HTTP 1.1的緩衝規範時,這種緩衝效果最好。如果伺服器是SOAP伺服器(或者實作類別似於RPC伺服器或RESTful伺服器),就需要用資料模型緩衝。如果伺服器遵循HTTP 1.1緩衝規範,就用URL緩衝。資料模型緩衝允許用戶端(iOS應用)掌控緩衝失效的情形,當開發人員實現URL緩衝時,伺服器通過HTTP 1.1的緩衝控制頭控制緩衝失效。儘管有些程式員覺得這種方式違反直覺,而且實現起來也很複雜(尤其是在伺服器端),但這可能是實現緩衝的好辦法。事實上,MKNetworkKit提供了對HTTP 1.1緩衝標準的原生支援。
資料模型緩衝:
本節我們來給第14章中的iHotelApp添加用資料模型緩衝實現的按需緩衝。按需緩衝是在視圖從視圖階層中消失時做的(從技術上講,是在viewWillDisappear:方法中)。支援緩衝的視圖控制器的基本結構24-1所示。AppCache Architecture的完整代碼可從本章的下載原始碼中找到。後面講解的內容假設你已經下載了代碼並且可以隨時使用。
圖24-1
實現了按需緩衝的視圖控制器的控制流程
在viewWillAppear方法中,查看緩衝中是否有顯示這個視圖所需的資料。如果有就擷取資料,再用快取資料更新使用者介面。然後檢查緩衝中的資料是否已經到期。你的商務規則應該能夠確定什麼是新資料、什麼是舊資料。如果內容是舊的,把資料顯示在UI上,同時在後台從伺服器擷取資料並再次更新UI。如果緩衝中沒有資料,顯示一個轉動的圓圈表示正在載入,同時從伺服器擷取資料。得到資料後,更新UI。
前面的流程圖假定顯示在UI上的資料是可以歸檔的模型。在iHotelApp的MenuItem模型中實現NSCoding協議。NSKeyedArchiver需要模型實現這個協議,如下面的程式碼片段所示。
MenuItem類的encodeWithCoder方法(MenuItem.m)
- (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:self.itemId forKey:@"ItemId"]; [encoder encodeObject:self.image forKey:@"Image"]; [encoder encodeObject:self.name forKey:@"Name"]; [encoder encodeObject:self.spicyLevel forKey:@"SpicyLevel"]; [encoder encodeObject:self.rating forKey:@"Rating"]; [encoder encodeObject:self.itemDescription forKey:@"ItemDescription"]; [encoder encodeObject:self.waitingTime forKey:@"WaitingTime"]; [encoder encodeObject:self.reviewCount forKey:@"ReviewCount"]; }
MenuItem類的initWithCoder方法(MenuItem.m)
- (id)initWithCoder:(NSCoder *)decoder {if ((self = [super init])) { self.itemId = [decoder decodeObjectForKey:@"ItemId"]; self.image = [decoder decodeObjectForKey:@"Image"]; self.name = [decoder decodeObjectForKey:@"Name"]; self.spicyLevel = [decoder decodeObjectForKey:@"SpicyLevel"]; self.rating = [decoder decodeObjectForKey:@"Rating"]; self.itemDescription = [decoder decodeObjectForKey:@"ItemDescription"]; self.waitingTime = [decoder decodeObjectForKey:@"WaitingTime"]; self.reviewCount = [decoder decodeObjectForKey:@"ReviewCount"]; }return self; }
就像之前提到過的,可以用Accessorizer來產生NSCoding協議的實現。
根據圖24-1中的緩衝流程圖,我們需要在viewWillAppear:中實現實際的緩衝邏輯。把下面的代碼加入viewWillAppear:就可以實現。
視圖控制器的viewWillAppear:方法中從緩衝恢複資料模型對象的程式碼片段
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];
緩衝機制的邏輯流如下所示。
- 視圖控制器在歸檔檔案MenuItems.archive中檢查之前緩衝的項並反歸檔。
- 如果MenuItems.archive不存在,視圖控制器調用方法從伺服器擷取資料。
- 如果MenuItems.archive存在,視圖控制器檢查歸檔檔案的修改時間以確認快取資料有多舊。如果資料到期了(由業務需求決定),再從伺服器擷取一次資料。否則顯示緩衝的資料。
接下來,把下面的代碼加入viewDidDisappear方法可以把模型(以NSKeyedArchiver的形式)儲存在Library/Caches目錄中。
視圖控制器的viewWillDisappear:方法中快取資料模型的程式碼片段
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數組的內容儲存在歸檔檔案中。注意,如果不是在viewWillAppear:方法中從伺服器擷取資料的話,這種情況不能緩衝。
所以,只需在視圖控制器中加入不到10行的代碼(並將Accessorizer產生的幾行代碼加入模型),就可以為應用添加緩衝支援了。
重構
當開發人員有多個視圖控制器時,前面的代碼可能會有冗餘。我們可以通過抽象出公用代碼並移入名為AppCache的新類來避免冗餘。AppCache是處理緩衝的應用的核心。把公用代碼抽象出來放入AppCache可以避免viewWillAppear:和viewWillDisappear:中出現冗餘代碼。
重構這部分代碼,使得視圖控制器的viewWillAppear/viewWillDisappear代碼塊看起來如下所示。加粗部分顯示重構時所做的修改,我會在代碼後面解釋。
視圖控制器的viewWillAppear:方法中用AppCache類快取資料模型的重構程式碼片段(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類把判斷資料是否到期的邏輯從視圖控制器中抽象出來了,還把緩衝儲存的位置也抽象出來了。稍後在本章中我們還會修改AppCache,再引入一層緩衝,內容會儲存在記憶體中。
因為AppCache抽象出了緩衝的儲存位置,我們就不需要為複製粘貼代碼來獲得應用的緩衝目錄而操心了。如果應用類似於iHotelApp,開發人員可通過為每個使用者建立子目錄即可輕鬆增強快取資料的安全性。然後我們就可以修改AppCache中的輔助方法,現在它返回的是緩衝目錄,我們可以讓它返回當前登入使用者的子目錄。這樣,一個使用者緩衝的資料就不會被隨後登入的使用者看到了。
完整的代碼可以從本書網站上本章的原始碼下載中擷取。
緩衝版本控制:
我們在上一節中寫的AppCache類從視圖控制器中抽象出了按需緩衝。當視圖出現和消失時,緩衝就在幕後工作。然而,當你更新應用時,模型類可能會發生變化,這意味著之前歸檔的任何資料將不能恢複到新的模型上。正如之前所講,對按需緩衝來說,資料並沒有那麼重要,開發人員可以刪除資料並更新應用。我會展示可以用來在版本升級時刪除緩衝目錄的程式碼片段。
iOS中驗證模型:
第二個是驗證模型,伺服器通常會發送一個校正和(Etag)。後續所有從緩衝獲得資源的請求都應該用這個校正和向伺服器**重新驗證**資源是否有變化。如果校正和匹配,伺服器就返回一個HTTP 304 Not Modified的狀態代碼。
IOS記憶體緩衝:
目前為止,所有iOS裝置都帶有快閃記憶體,而快閃記憶體有點小問題:它的讀寫壽命是有限的。儘管這個壽命跟裝置的使用壽命比起來很長,但是仍然需要避免過於頻繁地讀寫快閃記憶體。在上一個例子中,視圖隱藏時是直接緩衝到磁碟的,而視圖顯示時又是直接從磁碟讀取的。這種行為會使使用者裝置的緩衝負擔很重。為避免這個問題,我們可以再引入一層緩衝,利用裝置的RAM而不是快閃記憶體(用NSMutableDictionary)。在24.2.1節的“實現資料模型緩衝”中,我們介紹了建立歸檔的兩種方法:一個是儲存到檔案,另一個是儲存為NSData對象。這次會用到第二個方法,我們會得到一個NSData指標,將該指標儲存到NSMutableDictionary中,而不是檔案系統裡的一般檔案。引入記憶體緩衝的另一個好處是,在歸檔和反歸檔內容時效能會略有提升。聽起來很複雜,實際上並不複雜。本節將介紹如何給AppCache類添加一層透明的、位於記憶體中的緩衝。(“透明”是指調用代碼,即視圖控制器,甚至不知道這層緩衝的存在,而且也不需要改動任何代碼。)我們還會設計一個LRU(Least Recently Used,最近最少使用)演算法來把緩衝的資料儲存到磁碟。
以下簡單列出了要建立記憶體緩衝需要的步驟。這些步驟將會在下面幾節中詳細解釋。
- 添加變數來存放記憶體快取資料。
- 限制記憶體緩衝大小,並且把最近最少使用的項寫入檔案,然後從記憶體緩衝中刪除。RAM是有限的,達到使用極限就會觸發記憶體警告。收到警告時不釋放記憶體會使應用崩潰。我們當然不希望發生這種事,所以要為記憶體緩衝設定一個最大閾值。當緩衝滿了以後再添加任何東西時,最近最少使用的對象應該被儲存到檔案(快閃記憶體中)。
- 處理記憶體警告,並把記憶體緩衝以檔案形式寫入快閃記憶體。
- 當應用關閉、退出,或進入後台時,把記憶體緩衝全部以檔案形式寫入快閃記憶體。
IOS緩衝機制詳解