NSCache:专门做缓存的类
NSCache简介:NSCache是苹果官方提供的缓存类,用法与NSMutableDictionary的用法很相似,在AFNetworking和SDWebImage中,使用它来管理缓存。
NSCache在系统内存很低时,会自动释放一些对象(出自苹果官方文档,不过在模拟器中模拟内存警告时,不会做缓存的清理动作) 为了确保接收到内存警告时能够真正释放内存,最好调用一下removeAllObjects方法。
NScache是线程安全的,在多线程操作中,不需要对Cache加锁。
NScache的key只是做强引用,不需要实现NScopying协议。
NSCache的属性:
delegate代理属性
totalCostLimit :缓存空间的最大成本,超出上限会自动回收对象。默认值是0没有限制。
countLimit:能够缓存对象的最大数量,默认值也是0(默认没有限制)。
(当超出缓存最大成本或数量时,NSCache会把前面的数据即最开始存的给清除掉)
evictsObjectsWithDiscardedContent:标示是否回收废弃的内容,默认值是YES(自动回收)。
NSCache的方法:
-objectForKey:返回与键值关联的对象。
-setObject: forKey: 在缓存中设置指定键名对应的值。与可变字典不同的是,缓存对象不会对键名做copy操作 0成本
-setObject: forKey: cost: 在缓存中设置指定键名对应的值,并且指定该键值对的成本。成本cost用于计算记录在缓冲中所有对象的总成本。当出现内存警告,或者超出缓存的成本上限时,缓存会开启一个回收过程,删除部分元素。
-removeObjectForKey:删除缓存中指定键名的对象。
-removeAllObjects:删除缓存中的所有对象。
委托方法:
-cache: willEvictObject: 缓存将要删除对象时调用,不能在此方法中修改缓存。仅仅用于后台的打印,以便于程序员的测试。
举例验证如下:
//
// ViewController.m
// NScache的演练
//
// Created by apple on 15/10/24.
// Copyright (c) 2015年 LiuXun. All rights reserved.
//
#import "ViewController.h"
@interface ViewController ()
// 缓存的容器
@property (nonatomic, strong) NSCache *myCache;
@end
@implementation ViewController
-(NSCache *)myCache
{
if (_myCache == nil) {
_myCache = [[NSCache alloc] init];
/** NSCache类以下几个属性用于限制成本
NSUInteger totalCostLimit "成本" 限制,默认是0(没有限制)
NSUInteger countLimit 数量的限制 默认是0 (没有限制)
// 设置缓存的对象,同时指定限制成本
-(void)setObject:(id) obj forKey:(id) key cost:(NSUInteger) g
*/
// 设置数量限额。一旦超出限额,会自动删除之前添加的东西
_myCache.countLimit = 30; // 设置了存放对象的最大数量
_myCache.delegate = self;
}
return _myCache;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for (int i =0 ; i< 100; i++) {
// 向缓存中添加对象
NSString *str = [NSString stringWithFormat:@"hello - %d", i];
[self.myCache setObject:str forKey:@(i)]; // @(i) 相当于 [NSNumber numberWith......]
}
for (int i=0 ; i< 100; i++) {
NSLog(@"%@", [self.myCache objectForKey:@(i)]);
}
}
// NSCache的代理方法只有一个
// 告诉即将要被删除的对象
-(void)cache:(NSCache *)cache willEvictObject:(id)obj
{
// 此代理方法主要用于程序员的测试
NSLog(@"要删除的对象obj-------------%@", obj);
}
@end
没有设置代理时的运行结果如下:
因为限额最大数量是30,所以NSCache把前面的0~69的对象全部删除清理了。
遵循代理方法后,打印结果如下:
发现此代理方法就是在清理缓存对象之前告知程序员,便于调试而已。
所以在原来的工程中做缓存池的可变字典都可以用NScache进行替代了。代码如下:
CZApp.h
//
// CZApp.h
// NSoperation之网络图片下载
//
// Created by apple on 15/10/23.
// Copyright (c) 2015年 LiuXun. All rights reserved.
//
#import
@interface CZApp : NSObject
@property(nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *icon;
@property (nonatomic, strong) NSString *download;
/**
保存网络下载的图像
*/
// @property(nonatomic, strong) UIImage *image;
+(instancetype) appWithDict:(NSDictionary *) dict;
@end
CZApp.m
//
// CZApp.m
// NSoperation之网络图片下载
//
// Created by apple on 15/10/23.
// Copyright (c) 2015年 LiuXun. All rights reserved.
//
#import "CZApp.h"
@implementation CZApp
+(instancetype) appWithDict:(NSDictionary *) dict
{
CZApp *app = [[self alloc] init];
[app setValuesForKeysWithDictionary:dict];
return app;
}
@end
viewController.m
//
// ViewController.m
// NSoperation之网络图片下载
//
// Created by apple on 15/10/23.
// Copyright (c) 2015年 LiuXun. All rights reserved.
//
#import "ViewController.h"
#import "CZApp.h"
@interface ViewController ()
// plist文件数据的容器
@property (nonatomic, strong) NSArray *appList;
// 管理下载的全局队列
@property (nonatomic, strong) NSOperationQueue *opQueue;
/**所有下载的缓冲池*/
@property (nonatomic, strong) NSMutableDictionary *operationCache;
/**保存所有图像的缓存*/
@property (nonatomic, strong) NSCache *imageCache;
@end
@implementation ViewController
// 懒加载
-(NSArray *)appList
{
if (_appList == nil) {
NSArray *dicArray = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]];
// 字典转模型
NSMutableArray *arryM = [NSMutableArray array];
for(NSDictionary *dict in dicArray){
CZApp *app = [CZApp appWithDict:dict];
[arryM addObject:app];
}
_appList = arryM;
}
return _appList;
}
-(NSOperationQueue *)opQueue
{
if (_opQueue == nil) {
_opQueue = [[NSOperationQueue alloc] init];
}
return _opQueue;
}
-(NSMutableDictionary *)operationCache
{
if (_operationCache == nil) {
_operationCache = [[NSMutableDictionary alloc] init];
}
return _operationCache;
}
-(NSCache *)imageCache
{
if (_imageCache == nil) {
_imageCache = [[NSCache alloc] init];
}
return _imageCache;
}
#pragma mark - 实现数据源方法
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.appList.count;
}
/**
问题1:如果网速比较慢,会很卡
解决方法:使用异步下载
问题2:图片没有Frame,所有cell初始化的时候,给imageView的frame是0。异步下载完之后不显示 解决办法:使用占位图(如果展位图比较大, 自定义cell可以解决)
问题3:如果图片下载速度不一致,同时用户快速滚动的时候,会因为Cell的重用导致图片混乱
解决方法:MVC,使用Model(模型)保存下载的图像,再次刷新表格。
问题4:在用户快速滚动的时候,会重复添加下载任务到下载队列。
解决方法:建立下载操作的缓冲池。首先检查缓冲池里是否有当前图片的下载操作。有的话就不创建下载操作。从而保证一张图片只添加一个下载操作。其实就是建立一个全局的字典属性。
问题5: 将图片保存到模型里的优缺点
优点:不用重复下载,利用MVC刷新表格,不会造成数据混乱,加载速度比较快
缺点:内存,所有下载好的图像都会记录在模型里。如果数据比较多(2000)个就会造成内存警告。
-***图像根模型耦合性太强。导致清理内存非常困难
解决办法:模型跟图像分开。在控制器里做缓存。
问题6:下载操作缓冲池会越来越大。需要及时清理。
*/
/**
代码重构:1.如果代码太长。
目的:
- 写的时候,如果思路清楚,能够一次性写完,但是也要注意同构。
- 时间长了,不好阅读
- 重构代码,便于维护
重构方法:
如果有一部分代码专门解决某一问题,就封装起来。
1. 新建一个方法—> 剪切代码。
2. 传参数。
3. 在原来剪切代码的地方,调用抽取的方法。
4. 注意,测试。
5. 注意if嵌套,在实际的开发,非常忌讳很深的嵌套。
*/
-(void)viewDidLoad
{
NSLog(@"%@", [self cachePathWithUrl:@""]);
}
// cell里面的imageView子控件是懒加载
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *ID = @"AppCell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:ID];
// 给Cell设置数据
CZApp *app = self.appList[indexPath.row];
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;
// 判断模型里面是否有图像
if ([self.imageCache objectForKey:app.icon]) { // 内存有图片
NSLog(@" 图片已经下载......");
cell.imageView.image = [self.imageCache objectForKey:app.icon];
}else{ // 内存无图片
// 显示图片
// 如果沙盒里面有图片,直接从沙盒加载
UIImage *image = [UIImage imageWithContentsOfFile:[self cachePathWithUrl:app.icon]];
if (image) { // 沙盒有图片
NSLog(@"直接从沙盒加载图片");
// 1. 设置图片缓存到内存,方便下次从内存直接加载
[self.imageCache setObject:image forKey:app.icon];
// 2. 显示图片到cell
cell.imageView.image = [self.imageCache objectForKey:app.icon];
}else{ // 沙盒没有图片
// 显示图片—占位图
cell.imageView.image = [UIImage imageNamed:@"user_default"];
#warning 从这里开始剪切的代码
// 下载图片
[self downloadImage:indexPath];
}
}
return cell;
}
-(void)downloadImage:(NSIndexPath *)indexPath
{
CZApp *app = self.appList[indexPath.row];
/**
如果下载缓冲池里面有当前图片的下载操作,就不用创建下载操作,没有才创建
缓冲池字典中 key:存放当前图片的url,字符串类型。
Value:保存下载操作
*/
if (self.operationCache[app.icon]) {
NSLog(@"正在玩命下载中......");
return;
}
// 缓冲池没有下载操作
// 异步下载图片
__weak typeof(self) weakSelf = self;
NSBlockOperation *downLoadOp = [NSBlockOperation blockOperationWithBlock:^{
// 模拟延时
[NSThread sleepForTimeInterval:2];
NSLog(@"正在下载中......");
// 1. 下载图片(二进制数据)
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:app.icon]];
UIImage *image = [UIImage imageWithData:data];
// 2. 将下载的数据保存到沙盒
// 字典的赋值不能为nil,赋值为nil会崩溃
if (image) {
// 先保存到图片缓存
[weakSelf.imageCache setObject:image forKey:app.icon];
// 将图片写入到沙盒
[data writeToFile:[self cachePathWithUrl:app.icon] atomically:YES];
}
// 3 将操作从缓冲池删除——将下载好的图片操作从缓冲池中移除
[weakSelf.operationCache removeObjectForKey:app.icon];
// 4. 在主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
/** reload 会重新调用cell的初始化方法, 会重新判断模型里面是否有图像
有的话就直接显示
*/
}];
}];
// 将操作添加到队列
[weakSelf.opQueue addOperation:downLoadOp];
NSLog(@"操作的数量------------->%ld", self.opQueue.operationCount);
// 将操作添加到缓冲池中(使用图片的url作为key)
[weakSelf.operationCache setObject:downLoadOp forKey:app.icon];
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{ // 点击后查看操作缓冲池内的操作详情
NSLog(@"%@", self.operationCache);
}
/**
在真实开发中,一定要注意这个方法
*/
-(void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// 需要在这里做一些内存清理的工作,如果不处理会被系统强制闪退
// 清理内存
[self.imageCache removeAllObjects];
// 清理操作的缓存
[self.operationCache removeAllObjects];
// 取消下载队列内的任务
[self.opQueue cancelAllOperations];
}
/**
拼接一个文件在沙盒的全路径
*/
-(NSString *)cachePathWithUrl:(NSString *)urlStr
{ // 将图片网址名作为作为最后一项
// 1. 获得缓存的路径
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 2. 把路径根urlStr拼接起来
return [cachePath stringByAppendingPathComponent:urlStr.lastPathComponent];
}
-(void)dealloc
{
NSLog(@"销毁控制器-------------");
}
@end