[轉]iOS緩存機制詳解

資料均來自互聯網,轉載時請務必以超連接形式標明文章 原始出處 、做者信息和本聲明。不然將追究法律責任。html

人魔七七:http://www.cnblogs.com/qiqibo/程序員

爲何要有緩存算法

應用須要離線工做的主要緣由就是改善應用所表現出的性能。將應用內容緩存起來就能夠支持離線。咱們能夠用兩種不一樣的緩存來使應用離線工做。第一種是**按需緩存**,這種狀況下應用緩存起請求應答,就和Web瀏覽器的工做原理同樣;第二種是**預緩存**,這種狀況是緩存所有內容(或者最近n條記錄)以便離線訪問。sql

像第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緩存的實現原理和瀏覽器緩存或代理服務器緩存相似。當服務器設計得體,遵循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的完整代碼可從本章的下載源代碼中找到。後面講解的內容假設你已經下載了代碼而且能夠隨時使用。

Image

圖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];
複製代碼

緩存機制的邏輯流以下所示。

  1. 視圖控制器在歸檔文件MenuItems.archive中檢查以前緩存的項並反歸檔。
  2. 若是MenuItems.archive不存在,視圖控制器調用方法從服務器獲取數據。
  3. 若是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,最近最少使用)算法來把緩存的數據保存到磁盤。

如下簡單列出了要建立內存緩存須要的步驟。這些步驟將會在下面幾節中詳細解釋。

  1. 添加變量來存放內存緩存數據。
  2. 限制內存緩存大小,而且把最近最少使用的項寫入文件,而後從內存緩存中刪除。RAM是有限的,達到使用極限就會觸發內存警告。收到警告時不釋放內存會使應用崩潰。咱們固然不但願發生這種事,因此要爲內存緩存設置一個最大閾值。當緩存滿了之後再添加任何東西時,最近最少使用的對象應該被保存到文件(閃存中)。
  3. 處理內存警告,並把內存緩存以文件形式寫入閃存。
  4. 當應用關閉、退出,或進入後臺時,把內存緩存所有以文件形式寫入閃存。
相關文章
相關標籤/搜索