码迷,mamicode.com
首页 > 移动开发 > 详细

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

时间:2016-07-13 16:31:52      阅读:254      评论:0      收藏:0      [点我收藏+]

标签:

#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

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!