您的位置:首页 > 移动开发 > IOS开发

iOS 【Multithreading-多图下载数据展示案例(二级缓存)/模拟SDWebImage内部实现】

2016-07-12 19:53 573 查看
#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,而作者也提出了解决方案)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息