多线程 - 11.图片缓存技术(SDWebImage框架内部实现)

1.图片缓存概述

  • 在iOS项目开发中,我们经常需要从网络上获取图片显示到我们的界面上,示例界面
    图片Image
  • 利用UITableView实现上述应用,若是我们直接在设置cell内容的数据源方法中直接通过从网络上获取的数据设置图片,会造成两个问题:
    • 我们一般将耗时操作放在子线程中执行,若是放在主线程中(当图片资源较大时)会阻塞主线程
    • 另外,在我们滑动cell时,会造成图片的重复下载

2.解决图片重复下载问题

  • 要想实现每个cell的图片只下载一次,可以利用缓存来实现
// 首先定义一个全局的属性,可变字典保存下载的图片
// 利用一个URL字符串对应一张图片的思想,缓存下载的图片
@property (nonatomic, strong) NSMutableDictionary *imageCaches; 
// 然后通过懒加载技术创建可变字典
- (NSMutableDictionary *)imageCaches
{
    if (!_imageCaches) {
        _imageCaches = [NSMutableDictionary dictionary];
    }
    return _imageCaches;
}
// 之后,在数据源方法中,在加载图片时,先从图片内存缓存中去取,若是内存中不存在图片,那么就去网络上去下载
  • 但是仅仅加载到内存中,一旦程序退出后再重新启动,又需要重新加载图片,所以我们还要将下载的图片缓存到磁盘
  • 应用程序一启动,就先从内存中查找是否有缓存图片,若是没有就再去磁盘缓存中找,若是都没有找到就再去下载图片,并将图片缓存到内存和磁盘
  • 之所以要将图片加载到内存,是为了提供程序运行效率
  • 所以最终实现方法如下:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    // 创建cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    // 设置cell的数据
    NSDictionary *dict = self.apps[indexPath.row];
    cell.textLabel.text = dict[@"name"];
    cell.detailTextLabel.text = dict[@"download"];

    // 加载图片
    // 1.从内存中取
    NSString *urlStr = dict[@"icon"];
    UIImage *img = self.imageCaches[urlStr];
    if(img == nil){// 内存缓存中不存在图片
        // 2.从磁盘缓存中去取
        NSString *filePath = [dict[@"icon"] cacheDir];
        NSData *data = [NSData dataWithContentsOfFile:filePath];
        if (data == nil) { // 2.1磁盘缓存也为空,就去网络上下载
            NSURL *url = [NSURL URLWithString:dict[@"icon"]];
            NSData *data = [NSData dataWithContentsOfURL:url];
            img = [UIImage imageWithData:data];
            // 缓存到内存
            self.imageCaches[urlStr] = img;
            // 缓存到磁盘
            [data writeToFile:filePath atomically:YES];
            // 更新UI
           cell.imageView.image = img;
        }else{ // 2.2 磁盘缓存中存在
            NSLog(@"使用磁盘缓存");
            // 从磁盘缓存中读取图片
            img = [UIImage imageWithData:data];
            // 缓存到内存
            self.imageCaches[urlStr] = img;
            // 更新UI
            cell.imageView.image = img;
        }
    }else{ // 3.内存中存在图片
        NSLog(@"使用内存缓存");
        // 直接更新UI
       cell.imageView.image = img;
    }
    // 返回cell
    return cell;
}
  • 可以看到第一次运行程序时,会直接从网络上下载,当全部下载完毕后,再次刷新cell时,将输出使用内存缓存
  • 若是关闭程序重新运行,那么会先输出使用磁盘缓存,当全部cell加载完毕后,再次刷新cell会输出使用内存缓存

3.解决耗时操作阻塞主线程问题

  • 一般情况下我们会将耗时操作放在子线程中执行
  • 所以,这里我们需要将下载图片的操作放在子线程中执行
    // 耗时操作放在子线程中
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
        NSURL *url = [NSURL URLWithString:dict[@"icon"]];
        NSData *data = [NSData dataWithContentsOfURL:url];
        img = [UIImage imageWithData:data];
        // 缓存到内存
        self.imageCaches[urlStr] = img;
        // 缓存到磁盘
        [data writeToFile:filePath atomically:YES];
        // 回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // 更新UI
            cell.imageView.image = img;
        }];
    }];
    [queue addOperation:op];
  • 见上述代码,确实可以实现将耗时操作放在子线程中,不阻塞主线程,但是又会引发新的问题
  • 问题1:因为cell的重用机制,以及下载图片时耗时操作,当一行cell的图片还未下载完毕的时候被另一行cell重用了,会导致图片被重复设置,可能会产生图片一闪的情况
  • 问题2:若是用户拖拽的太快的时候,当一行cell先被移除视野又马上被移回视野的时候,因为前一个下载任务还未执行完毕,又重新给cell添加了一个新的下载任务,导致重复下载的情况

  • 最终解决方案:

  • 添加操作缓存,在添加操作在队列之前,先给每个URL字符串绑定下载操作,在执行下载操作前先判断当前cell是否有下载任务,若是没有再去添加新的下载任务,并且在回到主线程刷新UI的时候移除任务
  • 另外,为了解决由于初始情况图片不显示,待拖动后图片才显示的问题,需要先给cell设置一样大小的占位图片,带下载好图片在主线程更新UI后就能够在初始时显示图片
    // 2.1 磁盘缓存也为空,就去网络上下载
    // 2.2 判断当前是否有下载任务
    NSOperation *op = self.operationCaches[dict[@"icon"]];
    if (op == nil){//当前cell的任务为空,就创建新的下载任务
        // 耗时操作放在子线程中
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:dict[@"icon"]];
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 2.3判断当前是否下载成功
        if (data == nil) { //下载失败,应该将当前图片对应的下载任务从缓存中移除以便于下次可以再次尝试下载
            [self.operationCaches removeObjectForKey:dict[@"icon"]];
            return;
        }
        // 2.4 缓存图片
        img = [UIImage imageWithData:data];
        // 缓存到内存
        self.imageCaches[urlStr] = img;
        // 缓存到磁盘
        [data writeToFile:filePath atomically:YES];
        // 2.5 回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // 更新UI,刷新当前行
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            // 从缓存中将当前图片对应的下载任务移除
            [self.operationCaches removeObjectForKey:dict[@"icon"]];
                    }];
        }];
        [queue addOperation:op];
            }

4.补充说明

  • 这里仅仅说明了SDWebImage缓存图片的内部实现原理(有很多的小bug和细节未补充完成)
  • 但是SDWebImage内部将这些bug和细节都补充的非常完善以及提供了非常强大好用的接口,所以建议在项目开发中涉及到图片缓存的使用这个框架就可以搞定一切,无需关心内部具体的实现,非常好用

你可能感兴趣的:(多线程)