一、应该用哪种缓存技术
在众多可以本地保存数据的技术中,有三种脱颖而出:URL缓存、数据模型缓存(利用NSKeyedArchiver)和Core Data。
假设你正在开发一个应用,需要缓存数据以改善应用表现出的性能,你应该实现按需缓存(使用数据模型缓存或URL缓存)。
另一方面,如果需要数据能够离线访问,而且具有合理的存储方式以便离线编辑,那么就用高级序列化技术(如Core Data)。
1.URL缓存
内存缓存我们可以使用sdk中的NSURLCache类。NSURLRequest需要一个缓存参数来说明它请求的url何如缓存数据的,我们先看下它的CachePolicy类型。
- (void)viewDidLoad
{
[superviewDidLoad];
NSString *paramURLAsString=@"http://blog.sina.com.cn/u/2526279194";
self.urlCache =[NSURLCachesharedURLCache];
[self.urlCache setMemoryCapacity:1*1024*1024];
//创建一个nsurl
self.url = [NSURL URLWithString:paramURLAsString];
//创建一个请求
self.request=[NSMutableURLRequest requestWithURL:self.url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30.0f];
[self.myWebView loadRequest:self.request];
}
这个例子中,我们请求url为http://blog.sina.com.cn/u/2526279194的网站。如果这个url被缓存了,我们直接从缓存中获取数据,否则从http://blog.sina.com.cn/u/2526279194站点上重新获取数据。我们设置了缓存大小为1M。
- (IBAction)reloadWebView:(UIButton *)sender {
//从请求中获取缓存输出
NSCachedURLResponse*response =[self.urlCachecachedResponseForRequest:self.request];
//判断是否有缓存
if (response!= nil){
NSLog(@"如果有缓存输出,从缓存中获取数据");
[self.request setCachePolicy:NSURLRequestReturnCacheDataDontLoad];
}
[self.myWebView loadRequest:self.request];
self.connection = nil;
NSURLConnection *newConnection =[[NSURLConnection alloc]initWithRequest:self.request
delegate:self
startImmediately:YES];
self.connection = newConnection;
}
使用下面代码,我将请求的过程打印出来
- (void) connection:(NSURLConnection*)connection
didReceiveResponse:(NSURLResponse *)response{
NSLog(@"将接收输出");
}
- (NSURLRequest *)connection:(NSURLConnection *)connection
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse*)redirectResponse{
NSLog(@"即将发送请求");
return(request);
}
- (void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data{
NSLog(@"接受数据");
NSLog(@"数据长度为= %lu", (unsigned long)[data length]);
}
-(NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse*)cachedResponse{
NSLog(@"将缓存输出");
return(cachedResponse);
}
- (void)connectionDidFinishLoading:(NSURLConnection*)connection{
NSLog(@"请求完成");
}
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError*)error{
NSLog(@"请求失败");
}
@end
第一次打印结果如下
2013-01-31 15:28:29.923NSURLCacheDemo[27848:907] 即将发送请求
2013-01-31 15:28:30.043NSURLCacheDemo[27848:907] 将接收输出
2013-01-31 15:28:30.045NSURLCacheDemo[27848:907] 接受数据
2013-01-31 15:28:30.047NSURLCacheDemo[27848:907] 数据长度为= 30047
2013-01-31 15:28:30.095NSURLCacheDemo[27848:907] 接受数据
2013-01-31 15:28:30.098NSURLCacheDemo[27848:907] 数据长度为= 3575
2013-01-31 15:28:30.102NSURLCacheDemo[27848:907] 接受数据
2013-01-31 15:28:30.104NSURLCacheDemo[27848:907] 数据长度为= 1482
2013-01-31 15:28:30.105NSURLCacheDemo[27848:907] 将缓存输出
2013-01-31 15:28:30.107NSURLCacheDemo[27848:907] 请求完成
第二次点击打印结果如下
2013-01-31 15:28:31.599NSURLCacheDemo[27848:907] 如果有缓存输出,从缓存中获取数据
2013-01-31 15:28:31.607NSURLCacheDemo[27848:907] 即将发送请求
2013-01-31 15:28:31.840NSURLCacheDemo[27848:907] 将接收输出
2013-01-31 15:28:31.843NSURLCacheDemo[27848:907] 接受数据
2013-01-31 15:28:31.845NSURLCacheDemo[27848:907] 数据长度为= 35104
2013-01-31 15:28:31.846NSURLCacheDemo[27848:907] 请求完成
我们看到没有“将缓存输出”一项,请求到的数据是第一次请求的累积,也就是第二次是从内存中获取数据的。
2.数据模型缓存
-》》数据存在闪存中
在viewWillAppear方法中,查看缓存中是否有显示这个视图所需的数据。如果有就获取数据,再用缓存数据更新用户界面。然后检查缓存中的数据是否已经过期。你的业务规则应该能够确定什么是新数据、什么是旧数据。如果内容是旧的,把数据显示在UI上,同时在后台从服务器获取数据并再次更新UI。如果缓存中没有数据,显示一个转动的圆圈表示正在加载,同时从服务器获取数据。得到数据后,更新UI。
我们需要在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];
重构
当开发者有多个视图控制器时,前面的代码可能会有冗余。我们可以通过抽象出公共代码并移入名为AppCache的新类来避免冗余。AppCache是处理缓存的应用的核心。把公共代码抽象出来放入AppCache可以避免viewWillAppear:和viewWillDisappear:中出现冗余代码。
重构这部分代码,使得视图控制器的viewWillAppear/viewWillDisappear代码块看起来如下所示。
-(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];
}
-》》数据存在RAM中
目前为止,所有iOS设备都带有闪存,而闪存有点小问题:它的读写寿命是有限的。尽管这个寿命跟设备的使用寿命比起来很长,但是仍然需要避免过于频繁地读写闪存。在上一个例子中,视图隐藏时是直接缓存到磁盘的,而视图显示时又是直接从磁盘读取的。这种行为会使用户设备的缓存负担很重。为避免这个问题,我们可以再引入一层缓存,利用设备的RAM而不是闪存(用NSMutableDictionary)。在24.2.1节的“实现数据模型缓存”中,我们介绍了创建归档的两种方法:一个是保存到文件,另一个是保存为NSData对象。这次会用到第二个方法,我们会得到一个NSData指针,将该指针保存到NSMutableDictionary中,而不是文件系统里的平面文件。引入内存缓存的另一个好处是,在归档和反归档内容时性能会略有提升。听起来很复杂,实际上并不复杂。本节将介绍如何给AppCache类添加一层透明的、位于内存中的缓存。(“透明”是指调用代码,即视图控制器,甚至不知道这层缓存的存在,而且也不需要改动任何代码。)我们还会设计一个LRU(Least Recently Used,最近最少使用)算法来把缓存的数据保存到磁盘。
以下简单列出了要创建内存缓存需要的步骤。这些步骤将会在下面几节中详细解释。
- 添加变量来存放内存缓存数据。
- 限制内存缓存大小,并且把最近最少使用的项写入文件,然后从内存缓存中删除。RAM是有限的,达到使用极限就会触发内存警告。收到警告时不释放内存会使应用崩溃。我们当然不希望发生这种事,所以要为内存缓存设置一个最大阈值。当缓存满了以后再添加任何东西时,最近最少使用的对象应该被保存到文件(闪存中)。
- 处理内存警告,并把内存缓存以文件形式写入闪存。
- 当应用关闭、退出,或进入后台时,把内存缓存全部以文件形式写入闪存。