标签:
#import "ViewController.h" #import "WZYApp.h" @interface ViewController () // 数据模型数组 @property (nonatomic, strong) NSArray *apps; // 保存操作对象的字典 @property (nonatomic, strong) NSMutableDictionary *operations; // 内存缓存 @property (nonatomic, strong) NSMutableDictionary *images; // 操作队列(防止重复创建) @property (nonatomic, strong) NSOperationQueue *queue; @end @implementation ViewController // 存放操作 -(NSMutableDictionary *)operations { if (_operations == nil) { _operations = [NSMutableDictionary dictionary]; } return _operations; } -(NSOperationQueue *)queue { if (_queue ==nil) { _queue = [[NSOperationQueue alloc]init]; } return _queue; } -(NSMutableDictionary *)images { if (_images == nil) { _images = [NSMutableDictionary dictionary]; } return _images; } -(NSArray *)apps { if (_apps == nil) { // 加载plist文件 NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]]; // 字典数组 -->模型数组 NSMutableArray *arrayM = [NSMutableArray array]; for (NSDictionary *dict in array) { [arrayM addObject:[WZYApp appWithDict:dict]]; } _apps = arrayM; } return _apps; } -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.apps.count; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSLog(@"---%@", [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]); //01 创建cell static NSString *ID = @"app"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID]; //02 设置cell的数据 //2.1 得到该行cell对应的数据 WZYApp *appM = self.apps[indexPath.row]; //2.2 设置标题 cell.textLabel.text = appM.name; //2.3 设置子标题 cell.detailTextLabel.text = appM.download; //2.4 设置图片 // 内存缓存(指一个字典属性)思路 /* 001 当把图片下载完成之后需要把该图片保存到内存缓存 002 在需要显示图片的时候,先检查本地的缓存中时候已经下载了该图片 003 如果缓存中有该图片,直接设置 004 如果缓存中没有改图片,此时需要去下载图片 */ // 磁盘缓存(沙盒Caches下)思路 /* 001 当图片下载完成之后除了保存到内存缓存中之外,还需要保存一份到磁盘缓存中 002 当图片需要显示的时候,先检查内存缓存,如果内存缓存中有数据那么就直接设置 003 如果内存缓存中没有数据,那么再去检查磁盘缓存 004 如果有数据,那么就直接拿来设置就可以 | 保存一份到内存缓存中 005 如果没有数据,那么这个时候再去下载数据 */ // 改善缓存结构(内存缓存--->二级缓存结构[内存缓存-沙盒缓存]) UIImage *image = [self.images objectForKey:appM.icon]; if (image) { // 内存缓存中有数据,就直接设置数据 cell.imageView.image = image; NSLog(@"第%zd行cell对应的图片已经存在,直接使用内存缓存",indexPath.row); } else { // 内存缓存中没有数据 // 获得磁盘缓存路径(三步) NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *fileName = [appM.icon lastPathComponent]; // 得到url中最后一个节点(从路径中获得完整的文件名,带后缀) NSString *fullPath = [caches stringByAppendingPathComponent:fileName]; // 拼接沙盒缓存路径(将上面两个字符串拼接) // 检查磁盘缓存 NSData *data = [NSData dataWithContentsOfFile:fullPath]; // data = nil; if (data) { // 磁盘缓存中有数据(模拟二次重启程序,内存缓存清空了,但是磁盘缓存还在,所以先将数据展示,然后再保存一份数据到内存缓存中) //显示图片 UIImage *image = [UIImage imageWithData:data]; cell.imageView.image = image; // 保存一份到内存缓存中 [self.images setObject:image forKey:appM.icon]; NSLog(@"%zd行cell对应的图片使用了磁盘缓存",indexPath.row); } else { // 磁盘缓存无数据(模拟首次进入程序,内存缓存和磁盘缓存都是空的。那么就先下载数据,然后再显示数据,接着保存一份数据到内存缓存,最后再保存一份数据到磁盘缓存) // 解决数据错乱问题(由于cell的重用原则,会重用cell及其内部数据) // 解决方案001 cell.imageView.image = nil;(但这样不好,如果网速很卡,用户会认为没有图片存在 // 解决方案002 设置占位图片,如下行代码 cell.imageView.image = [UIImage imageNamed:@"Snip20160712_43"]; // 解决图片重复下载问题(由于用户可能会不停拖拽界面,当cell重复出现在视野中并且网速较慢的时候,第一次cell进入的时候就已经创建好操作进行下载图片,但是此时cell若再次进入视野并且首次下载还未执行完,那么就会进行二次重复下载。) // 解决方案:先检查图片的下载操作是否已经存在 // 若 存在 等待就行(拦截二次下载) // 若 不存在 封装操作并且添加到队列(进行首次下载) NSBlockOperation *download = [self.operations objectForKey:appM.icon]; if (download == nil) { // 如果操作不存在 //封装操作 NSBlockOperation *download = [NSBlockOperation blockOperationWithBlock:^{ NSURL *url = [NSURL URLWithString:appM.icon]; for (NSInteger i = 0; i<1000000000; i++) { //模拟下载该图片需要花费较长的时间|网络不好的情况 } // 下载操作 NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; if (image == nil) { // 解决image为空时存到内存缓存报错问题(如果修改了数据,比如图片的url修改了,url还在但图片没有了,此时如果再执行将image保存到内存缓存(也就是字典中),是会报错的。因为nil不能往字典中存。) // 解决方案: // 要加一个判定if语句,如果数据不存在,就不要赋值,直接返回 // 解决网络卡顿下载失败情况下的再次下载问题 // 解决方案: // 将操作从缓存中移除(如果在下载的过程中网络中断,造成了下载失败,下载操作已经创建,但是下载任务还没有执行完毕。此时二次联网,再次执行下载操作,就不会再继续下载了。为什么呢?因为防止图片重复下载,操作已经创建之后就不会再次创建。那么这个情况下就要在判定image==nil的if中清空操作。也就是如果image没有成功设置,就清空下载操作,下次下载时再重新添加操作下载。) [self.operations removeObjectForKey:appM.icon]; return ; } // 把图片保存到内存缓存中 [self.images setObject:image forKey:appM.icon]; NSLog(@"下载%zd行cell对应的图片",indexPath.row); // 保存一份到磁盘缓存中 [data writeToFile:fullPath atomically:YES]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ // 解决图片不显示问题(拖动tableView才会显示。为什么呢?因为是异步执行,所以说没有等待cell.imageView.image设置成功就返回cell了。) // 解决方案: // 手动刷新一下cell的当前行,这样不用拖动也会显示数据了。 //[tableView reloadData]; 刷新整个tableView,耗费内存资源,不推荐 [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; // 刷新当前行 }]; }]; // 把操作缓存起来(用一个字典去接收保存操作对象,避免重复创建消耗内存) [self.operations setObject:download forKey:appM.icon]; // 添加操作到队列(执行操作中的内容) // 将下载操作保存到子线程中去执行,解决UI卡顿的问题。 [self.queue addOperation:download]; } else { // 如果操作不存在 //等着 NSLog(@"%zd行对应的图片已经正在下载,请等待....",indexPath.row); } } } //03 返回cell return cell; }
以上操作我们完全没有必要去写,因为十分繁琐,而且考虑到的情况还是有限的。我们可以用第三方框架SDWebImage帮我们实现下载图片二级缓存的操作。该框架内部还处理了很多我们暂时考虑不到的bug。省去了大量繁琐的工作。上面设置cell.imageView.image的操作共计100余行,我们可以用下面一行代码搞定:
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:appM.icon] placeholderImage:[UIImage imageNamed:@"Snip20160712_43"]];
注意一点:
直接用SDWebImage去设置image的时候,如果是在tableView上面设置,那么会因为imageView的尺寸没有提前设置而产生一些问题,所以我们需要提前设置好cell.imageView的尺寸。这时就需要自定义cell了。(SDWebImage这个框架是服务很多地方的,并不只是tableView一种,所以说会出现这种bug,而作者也提出了解决方案)
iOS 【Multithreading-多图下载数据展示案例(二级缓存)/模拟SDWebImage内部实现】
标签:
原文地址:http://blog.csdn.net/felicity294250051/article/details/51891945