iOS缓存机制详解

人魔七七:http://www.cnblogs.com/qiqibo/html

为何要有缓存程序员

应用须要离线工做的主要缘由就是改善应用所表现出的性能。将应用内容缓存起来就能够支持离线。咱们能够用两种不一样的缓存来使应用离线工做。第一种是**按需缓存**,这种状况下应用缓存起请求应答,就和Web浏览器的工做原理同样;第二种是**预缓存**,这种状况是缓存所有内容(或者最近n条记录)以便离线访问。算法

像第14章中开发的Web服务应用利用按需缓存技术来改善可感知的性能而不是提供离线访问。离线访问只是无意插柳的结果。Twitter和Foursquare就是很好的例子。这类应用获得的数据一般很快就会过期。对于一条几天前的推文或者朋友上周在哪里你能有多大兴趣?通常来讲,一条推文或者一条签到的信息只在几个小时内有意义,而24小时以后就变得可有可无。不过大部分Twitter客户端仍是会缓存推文,而Foursquare的官方客户端在无网络链接的状况下打开,会显示上次的状态。sql

你们能够用本身喜欢的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. 当应用关闭、退出,或进入后台时,把内存缓存所有以文件形式写入闪存。
相关文章
相关标签/搜索