网络(七):SDWebImage源码浅析

目录
一、我们先自己写一个加载网络图片的demo
 1、最简单的实现
 2、图片缓存处理:增加内存缓存
 3、图片缓存处理:增加磁盘缓存
 4、图片异步下载处理:把下载图片这个耗时操作放在子线程里去做
 5、图片异步下载处理:增加下载任务缓存
 6、防止内存溢出处理
 7、使用SDWebImage
二、SDWebImage源码分析
 1、图片异步下载模块
 2、图片缓存模块
 3、SDWebImageManager


一、我们先自己写一个加载网络图片的demo


1、最简单的实现

#import "CustomWebImageViewController.h"
#import "ImageModel.h"

@interface CustomWebImageViewController () 

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, copy) NSArray *dataArray;

@end

@implementation CustomWebImageViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"CustomWebImage";
    
    [self.view addSubview:self.tableView];
}


#pragma mark - UITableViewDataSource, UITableViewDelegate

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"reuseId" forIndexPath:indexPath];
    
    ImageModel *imageModel = self.dataArray[indexPath.row];
    cell.textLabel.text = imageModel.imageName;
    
    /*-----------注意这段代码-----------*/
    NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageModel.imageUrl]];
    UIImage *image = [UIImage imageWithData:imageData];
    cell.imageView.image = image;
    /*-----------注意这段代码-----------*/

    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 100;
}


#pragma mark - setter, getter

- (UITableView *)tableView {
    if (_tableView == nil) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 414, 736) style:(UITableViewStylePlain)];
        _tableView.dataSource = self;
        _tableView.delegate = self;
        [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"reuseId"];
    }
    return _tableView;
}

- (NSArray *)dataArray {
    if (_dataArray == nil) {
        NSArray *plistArray = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"data.plist" ofType:nil]];
        
        NSMutableArray *tempDataArray = [NSMutableArray array];
        [plistArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            [tempDataArray addObject:[ImageModel imageModelWithDictionary:obj]];
        }];
        
        _dataArray = tempDataArray;
    }
    return _dataArray;
}

@end

这样就写好了一个加载网络图片的demo,但是像上面这样给cell设置图片有两个严重的问题:

  • UI卡顿,原因就是我们把下载图片这个耗时操作放在主线程里做了,解决办法就是把下载图片这个耗时操作放在子线程里去做;
  • 图片重复下载,原因就是我们来回滑动tableView的时候,会反复触发cellForRowAtIndexPath:这个代理方法,进而导致滑出屏幕又滑进来的cell重复下载图片,解决办法就是增加图片缓存。

2、图片缓存处理:增加内存缓存

/// 内存缓存
@property (nonatomic, strong) NSCache *memoryCache;

- (NSCache *)memoryCache {
    if (_memoryCache == nil) {
        _memoryCache = [[NSCache alloc] init];
    }
    return _memoryCache;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"reuseId" forIndexPath:indexPath];
    
    ImageModel *imageModel = self.dataArray[indexPath.row];
    cell.textLabel.text = imageModel.imageName;
    
    /*-----------注意这段代码-----------*/
    // 尝试去内存缓存中读取图片
    UIImage *image = [self.memoryCache objectForKey:imageModel.imageUrl];
    if (image) { // 如果内存缓存中有这张图片,就拿来直接展示
        cell.imageView.image = image;
        
        NSLog(@"-----------图片来自内存缓存%@", imageModel.imageUrl);
    } else { // 如果内存缓存中没有这张图片,就从网络上下载,下载完成后拿来展示,并把这张图片存储到内存缓存中
        NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageModel.imageUrl]];
        image = [UIImage imageWithData:imageData];
        cell.imageView.image = image;
        
        // 存储到内存缓存
        [self.memoryCache setObject:image forKey:imageModel.imageUrl];
        
        NSLog(@"-----------图片来自网络:%@", imageModel.imageUrl);
    }
    /*-----------注意这段代码-----------*/

    return cell;
}

这样使用内存缓存就解决了App活着时的图片重复下载问题,但是还不完美,因为当我们杀掉App时,内存缓存就会销毁,这时再打开App,图片就还得从网络上下载,这种情况下还是存在图片重复下载问题,最好是能做到图片下载一次就够了,这样可以给用户省点流量,因此还需要增加磁盘缓存。

3、图片缓存处理:增加磁盘缓存

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"reuseId" forIndexPath:indexPath];
    
    ImageModel *imageModel = self.dataArray[indexPath.row];
    cell.textLabel.text = imageModel.imageName;
    
    /*-----------注意这段代码-----------*/
    /*
     沙盒目录:
     1、❌Documens:iTunes会同步,主要用来存放App的一些重要数据,我们需要主动清除(所以缓存数据不要存放在这里,iTunes同步缓存数据不像话)
     2、❌Library/Preference:iTunes会同步,主要用来存放App的一些偏好设置,我们需要主动清除(所以缓存数据不要存放在这里,iTunes同步缓存数据不像话)
     3、✅Library/Caches:iTunes不会同步,主要用来存放缓存,我们需要主动清除或者App内存不够时系统会自动清除一部分
     4、❌tmp:iTunes不会同步,主要用来存放临时文件,我们不需要主动清除,App运行期间随时可能被清除或者App退出后会被清除
     */
    // 尝试去内存缓存中读取图片(优先读取内存缓存,因为读取内存缓存肯定要比读取磁盘缓存快)
    UIImage *image = [self.memoryCache objectForKey:imageModel.imageUrl];
    if (image) { // 如果内存缓存中有这张图片,就拿来直接展示
        cell.imageView.image = image;
        
        NSLog(@"-----------图片来自内存缓存%@", imageModel.imageUrl);
    } else { // 如果内存缓存中没有这张图片,那就尝试去磁盘缓存中读取图片
        NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        NSString *fileName = imageModel.imageUrl.lastPathComponent;
        NSString *fullPath = [cachesPath stringByAppendingPathComponent:fileName];

        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
        if (imageData) { // 如果磁盘缓存中有这张图片,就拿来直接展示,并且把这张图片存储到内存缓存中,以便下次读取更快
            image = [UIImage imageWithData:imageData];
            cell.imageView.image = image;

            // 存储到内存缓存
            [self.memoryCache setObject:image forKey:imageModel.imageUrl];

            NSLog(@"-----------图片来自磁盘缓存:%@", imageModel.imageUrl);
        } else { // 如果磁盘缓存中也没有这张图片,就从网络上下载,下载完成后拿来展示,并把这张图片存储到内存缓存和磁盘缓存中
            imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageModel.imageUrl]];
            image = [UIImage imageWithData:imageData];
            cell.imageView.image = image;
            
            // 存储到内存缓存
            [self.memoryCache setObject:image forKey:imageModel.imageUrl];
            // 存储到磁盘缓存
            [imageData writeToFile:fullPath atomically:YES];

            NSLog(@"-----------图片来自网络:%@", imageModel.imageUrl);
        }
    }
    /*-----------注意这段代码-----------*/

    return cell;
}

4、图片异步下载处理:把下载图片这个耗时操作放在子线程里去做

/// 下载队列
@property (nonatomic, strong) NSOperationQueue *downloadQueue;

- (NSOperationQueue *)downloadQueue {
    if (_downloadQueue == nil) {
        _downloadQueue = [[NSOperationQueue alloc] init];
        // 实际开发中我们开三到五条线程就够了
        _downloadQueue.maxConcurrentOperationCount = 3;
    }
    return _downloadQueue;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"reuseId" forIndexPath:indexPath];
    
    ImageModel *imageModel = self.dataArray[indexPath.row];
    cell.textLabel.text = imageModel.imageName;
    
    /*-----------注意这段代码-----------*/
    /*
     沙盒目录:
     1、❌Documens:iTunes会同步,主要用来存放App的一些重要数据,我们需要主动清除(所以缓存数据不要存放在这里,iTunes同步缓存数据不像话)
     2、❌Library/Preference:iTunes会同步,主要用来存放App的一些偏好设置,我们需要主动清除(所以缓存数据不要存放在这里,iTunes同步缓存数据不像话)
     3、✅Library/Caches:iTunes不会同步,主要用来存放缓存,我们需要主动清除或者App内存不够时系统会自动清除一部分
     4、❌tmp:iTunes不会同步,主要用来存放临时文件,我们不需要主动清除,App运行期间随时可能被清除或者App退出后会被清除
     */
    // 尝试去内存缓存中读取图片(优先读取内存缓存,因为读取内存缓存肯定要比读取磁盘缓存快)
    __block UIImage *image = [self.memoryCache objectForKey:imageModel.imageUrl];
    if (image) { // 如果内存缓存中有这张图片,就拿来直接展示
        cell.imageView.image = image;
        
        NSLog(@"-----------图片来自内存缓存%@", imageModel.imageUrl);
    } else { // 如果内存缓存中没有这张图片,那就尝试去磁盘缓存中读取图片
        NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        NSString *fileName = imageModel.imageUrl.lastPathComponent;
        NSString *fullPath = [cachesPath stringByAppendingPathComponent:fileName];

        __block NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
        if (imageData) { // 如果磁盘缓存中有这张图片,就拿来直接展示,并且把这张图片存储到内存缓存中,以便下次读取更快
            image = [UIImage imageWithData:imageData];
            cell.imageView.image = image;

            // 存储到内存缓存
            [self.memoryCache setObject:image forKey:imageModel.imageUrl];

            NSLog(@"-----------图片来自磁盘缓存:%@", imageModel.imageUrl);
        } else { // 如果磁盘缓存中也没有这张图片,就从网络上下载,下载完成后拿来展示,并把这张图片存储到内存缓存和磁盘缓存中
            NSBlockOperation *downloadOperation = [NSBlockOperation blockOperationWithBlock:^{
                imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageModel.imageUrl]];
                image = [UIImage imageWithData:imageData];
                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    // 这样直接设置会导致图片显示不出来,因为图片的下载是在子线程里做的,在下载图片的过程中,cell就已经创建完了,但是因为图片没下载完,所以cell没有接收到图片,所以系统就把cell.imageView的大小搞成0了,所以即便下载完图片后我们再给cell设置一下图片也显示不出来了,因为这个时候cell.imageView大小为0
//                    cell.imageView.image = image;
                    
                    // 所以得采用刷新一下这行cell的办法,采用这个办法后会重新调用cellForRowAtIndexPath回调,其实已经是从内存缓存中读取图片了
                    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:(UITableViewRowAnimationNone)];
                }];
                
                // 存储到内存缓存
                [self.memoryCache setObject:image forKey:imageModel.imageUrl];
                // 存储到磁盘缓存
                [imageData writeToFile:fullPath atomically:YES];

                NSLog(@"-----------图片来自网络:%@", imageModel.imageUrl);
            }];
            [self.downloadQueue addOperation:downloadOperation];
        }
    }
    /*-----------注意这段代码-----------*/

    return cell;
}

这样就解决了UI卡顿问题,但是还不完美,因为图片的大小可能会很大、网速也可能会很慢,那当一个cell上的图片正在下载时,我们快速来回滑动tableView使得这个cell不断地滑出屏幕然后又出现,就会导致有多个任务都在下载这同一张图片,浪费用户的流量,因此还需要增加下载任务缓存。

5、图片异步下载处理:增加下载任务缓存

/// 下载任务缓存
@property (nonatomic, strong) NSCache *downloadOperationCache;

- (NSCache *)downloadOperationCache {
    if (_downloadOperationCache == nil) {
        _downloadOperationCache = [[NSCache alloc] init];
    }
    return _downloadOperationCache;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"reuseId" forIndexPath:indexPath];
    
    ImageModel *imageModel = self.dataArray[indexPath.row];
    cell.textLabel.text = imageModel.imageName;
    
    /*-----------注意这段代码-----------*/
    /*
     沙盒目录:
     1、❌Documens:iTunes会同步,主要用来存放App的一些重要数据,我们需要主动清除(所以缓存数据不要存放在这里,iTunes同步缓存数据不像话)
     2、❌Library/Preference:iTunes会同步,主要用来存放App的一些偏好设置,我们需要主动清除(所以缓存数据不要存放在这里,iTunes同步缓存数据不像话)
     3、✅Library/Caches:iTunes不会同步,主要用来存放缓存,我们需要主动清除或者App内存不够时系统会自动清除一部分
     4、❌tmp:iTunes不会同步,主要用来存放临时文件,我们不需要主动清除,App运行期间随时可能被清除或者App退出后会被清除
     */
    // 尝试去内存缓存中读取图片(优先读取内存缓存,因为读取内存缓存肯定要比读取磁盘缓存快)
    __block UIImage *image = [self.memoryCache objectForKey:imageModel.imageUrl];
    if (image) { // 如果内存缓存中有这张图片,就拿来直接展示
        cell.imageView.image = image;
        
        NSLog(@"-----------图片来自内存缓存%@", imageModel.imageUrl);
    } else { // 如果内存缓存中没有这张图片,那就尝试去磁盘缓存中读取图片
        NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        NSString *fileName = imageModel.imageUrl.lastPathComponent;
        NSString *fullPath = [cachesPath stringByAppendingPathComponent:fileName];

        __block NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
        if (imageData) { // 如果磁盘缓存中有这张图片,就拿来直接展示,并且把这张图片存储到内存缓存中,以便下次读取更快
            image = [UIImage imageWithData:imageData];
            cell.imageView.image = image;

            // 存储到内存缓存
            [self.memoryCache setObject:image forKey:imageModel.imageUrl];

            NSLog(@"-----------图片来自磁盘缓存:%@", imageModel.imageUrl);
        } else { // 如果磁盘缓存中也没有这张图片,就从网络上下载,下载完成后拿来展示,并把这张图片存储到内存缓存和磁盘缓存中
            // 尝试去下载任务缓存中读取下载任务
            NSBlockOperation *downloadOperation = [self.downloadOperationCache objectForKey:imageModel.imageUrl];
            if (!downloadOperation) { // 如果这张图片不在下载中,再开线程去下载,如果已经在下载中了,就不要重复下载了
                // 开始下载的时候先设置一个占位图
                cell.imageView.image = [UIImage imageNamed:@"loading"];
                
                downloadOperation = [NSBlockOperation blockOperationWithBlock:^{
                    imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageModel.imageUrl]];
                    image = [UIImage imageWithData:imageData];
                    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                        // 这样直接设置会导致图片显示不出来,因为图片的下载是在子线程里做的,在下载图片的过程中,cell就已经创建完了,但是因为图片没下载完,所以cell没有接收到图片,所以系统就把cell.imageView的大小搞成0了,所以即便下载完图片后我们再给cell设置一下图片也显示不出来了,因为这个时候cell.imageView大小为0
    //                    cell.imageView.image = image;
                        
                        // 所以得采用刷新一下这行cell的办法,采用这个办法后会重新调用cellForRowAtIndexPath回调,其实已经是从内存缓存中读取图片了
                        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:(UITableViewRowAnimationNone)];
                    }];
                    
                    // 存储到内存缓存
                    [self.memoryCache setObject:image forKey:imageModel.imageUrl];
                    // 存储到磁盘缓存
                    [imageData writeToFile:fullPath atomically:YES];

                    NSLog(@"-----------图片来自网络:%@", imageModel.imageUrl);
                }];
                [self.downloadQueue addOperation:downloadOperation];
            }
        }
    }
    /*-----------注意这段代码-----------*/

    return cell;
}

6、防止内存溢出处理

当加载的图片太多时,有可能会导致内存溢出,这时我们应该清除内存缓存中的图片并取消正在下载的任务(当然这个策略可以根据实际情况制定)。

- (void)didReceiveMemoryWarning {
    // 清除内存缓存中的图片
    [self.memoryCache removeAllObjects];
    
    // 取消正在下载的任务
    [self.downloadQueue cancelAllOperations];
}

7、使用SDWebImage

如果我们把上面的例子换成使用SDWebImage,代码将会非常简单。

#import "UIImageView+WebCache.h"

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"reuseId" forIndexPath:indexPath];
    
    ImageModel *imageModel = self.dataArray[indexPath.row];
    cell.textLabel.text = imageModel.imageName;
    
    /*-----------注意这段代码-----------*/
    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:imageModel.imageUrl] placeholderImage:[UIImage imageNamed:@"loading"] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
        NSLog(@"-----------图片下载进度:%f---%@", 1.0 * receivedSize / expectedSize, imageModel.imageUrl);
    } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
        NSLog(@"-----------图片下载完成:%d---%@", cacheType, imageURL);
    }];
    /*-----------注意这段代码-----------*/

    return cell;
}
---AppDelegate.m---

#import "AppDelegate.h"
#import "SDWebImageManager.h"

- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
    // 清除内存缓存中的图片
    [[SDWebImageManager sharedManager].imageCache clearMemory];
    
    // 取消正在下载的任务
    [[SDWebImageManager sharedManager] cancelAll];
}


二、SDWebImage源码分析


SDWebImage版本为3.8.3

其实SDWebImage的两大核心模块就是我们上面所实现的图片异步下载和图片缓存,当然除此之外它还提供了图片解码模块和一些辅助模块,然后基于这些模块又给我们提供了非常方便易用的UIImageView和UIButton的分类,还有一个批量下载图片工具类。

1、图片异步下载模块

-----------SDWebImageDownloader.m-----------

/// 下载队列
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
/// 下载任务缓存
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks;


/// init的伪代码
- (id)init {
    if ((self = [super init])) {
        _downloadQueue = [NSOperationQueue new];
        // 设置下载队列的最大并发数为6,也就是说最多可以同时下载6张图片
        _downloadQueue.maxConcurrentOperationCount = 6;
    }
    return self;
}

/// 图片异步下载的伪代码
- (void)downloadImageWithURL:(NSURL *)url {
    if (!self.URLCallbacks[url]) { // 如果URL对应的图片不在下载中再下载,避免重复下载
        // 添加到下载任务缓存中
        self.URLCallbacks[url] = callbacksForURL;
        
        // 根据要下载图片的URL创建一个request
        NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
        
        // 这里是一个自定义的operation,它把图片下载这个耗时操作封装在了它内部的start方法内
        SDWebImageDownloaderOperation *operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request progress:^(NSInteger receivedSize, NSInteger expectedSize) {
            NSLog(@"图片下载中的回调");
        } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
            NSLog(@"图片下载完成的回调");
        }];
        // 把operation添加到下载队列里,就会触发operation的start方法并开辟一条子线程来执行图片下载这个耗时操作了
        [self.downloadQueue addOperation:operation];
    }
}
-----------SDWebImageDownloaderOperation.m(继承自NSOperation)-----------

/// start函数的伪代码
- (void)start {
    // 使用NSURLSession发起一个网络请求来下载图片
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                          delegate:self
                                                     delegateQueue:nil];
    self.dataTask = [session dataTaskWithRequest:self.request];
    [self.dataTask resume];
}

#pragma mark - NSURLSessionDataDelegate

/// 图片下载中的回调
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [self.imageData appendData:data];
    
    // 触发SDWebImageDownloader图片下载中的回调
    if (self.progressBlock) {
        self.progressBlock(self.imageData.length, self.expectedSize);
    }
}

/// 图片下载完成的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    UIImage *image = [UIImage imageWithData:self.imageData];
    
    // 触发SDWebImageDownloader图片下载完成的回调
    if (self.completionBlock) {
        self.completionBlock(image, self.imageData, nil, YES);;
    }
}

可见SDWebImage实现图片异步下载的思路整体上跟我们自己写的差不多,也是通过NSOperation来把下载图片这个耗时操作放在子线程里去做,并且也做了下载任务缓存,当然它内部还有很多很多的细节值得深入学习。

2、图片缓存模块

  • 缓存图片
/// 内存缓存
@property (strong, nonatomic) NSCache *memCache;


/**
 * 方法1:把图片存储到磁盘缓存中(直接调用这个方法时,请注意要不要开子线程)
 *
 * @param imageData 要缓存的图片
 * @param key 图片的唯一标识,通常就是图片的URL
 */
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
    if (!imageData) {
        return;
    }
    
    // 创建磁盘缓存文件夹
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // 根据图片的URL获取默认的缓存路径(路径其实是URL经过MD5加密得到的)
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    
    // 把图片存储到磁盘缓存中
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
}

/**
 * 方法2:把图片存储到内存缓存中,选择性存储到磁盘缓存中
 */
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    
    // 把图片存储到内存缓存中(存储到内存缓存中非常快,所以就在主线程执行了)
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }

    if (toDisk) {
        // 开子线程把图片存储到磁盘缓存中
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;
            [self storeImageDataToDisk:data forKey:key];
        });
    }
}
  • 读取图片
/**
 * 方法1:读取内存缓存中的图片
 */
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    return [self.memCache objectForKey:key];
}

/**
 * 方法2:读取磁盘缓存中的图片
 */
- (UIImage *)diskImageForKey:(NSString *)key {
    NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
    if (data) {
        UIImage *image = [UIImage sd_imageWithData:data];
        image = [self scaledImageForKey:key image:image];
        if (self.shouldDecompressImages) {
            image = [UIImage decodedImageWithImage:image];
        }
        return image;
    }
    else {
        return nil;
    }
}

/**
 * 方法3:读取图片(来自内存缓存或磁盘缓存)
 *
 * @return operation,我们可以通过这个operation对象取消获取任务
 */
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // 首先去内存缓存中看看有没有这张图片,有的话就直接返回
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    // 内存缓存中没有的话再开个子线程去磁盘缓存中看看有没有这张图片,有的话就返回并把这张图片存储到内存缓存中,没有的话就返回个nil
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                // 存储到内存缓存中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

可见SDWebImage实现图片缓存的思路整体上跟我们自己写的也差不多,存储图片的时候也是有内存缓存和磁盘缓存两个级别;读取图片的时候也是首先去内存缓存中看看有没有这张图片,有的话就直接返回,没有的话再去磁盘缓存中看看有没有这张图片,有的话就返回并把这张图片存储到内存缓存中,没有的话就返回个nil;并且磁盘缓存的读写也都是在子线程做的;当然它内部还有很多很多的细节值得深入学习。

3、SDWebImageManager

接下来我们就看看SDWebImageManager是如何把上面两个模块给串起来的。

-----------SDWebImageManager.m-----------

/// 图片异步下载模块
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
/// 图片缓存模块
@property (strong, nonatomic, readwrite) SDImageCache *imageCache;


/// 加载一张网络图片的伪代码
// 当外界调用该方法加载一张网络图片的时候
- (id )downloadImageWithURL:(NSURL *)url
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    NSString *key = [self cacheKeyForURL:url];

    // 会首先调用图片缓存模块的方法,尝试读取该URL在内存缓存或磁盘缓存的图片
    [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
        if (image) { // 如果读取到了,就直接回调出去给外界使用,不发起网络请求
            dispatch_main_sync_safe(^{
                completedBlock(image, nil, cacheType, YES, url);
            });
        } else { // 如果没有读取到,就调用图片异步下载模块的方法去网络上下载图片
            [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                if (downloadedImage) { // 图片下载完成后
                    // 把图片存储到内存缓存和磁盘缓存中
                    [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:YES];

                    // 回调出去给外界使用
                    dispatch_main_sync_safe(^{
                        completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                    });
                }
            }];
        }
    }];

    return operation;
}

参考
1、图片的编码解码

位图(bitmap)是真正显示在屏幕上的东西,我们平常所说的PNG、JPEG格式的图片其实是位图编码后的产物,因为直接存储位图会占用较大的内存空间。反过来当我们从Bundle或磁盘上读取到一张PNG、JPEG格式的图片后,还得解码成原来的位图才能交给屏幕渲染出来。

例如实际开发中我们经常写下面这样的代码来显示一张图片:

UIImage *image = [UIImage imageNamed:@"test.png"]; // 会创建内存缓存
// UIImage *image = [UIImage imageWithContentsOfFile:@"test.png"]; // 不会创建内存缓存

UIImageView *imageView = [[UIImageView alloc] init];
imageView.image = image;

[self.view addSubview:imageView];

当我们获取到一张图片 && 把这张图片设置在了imageView上 && imageView被添加到了视图层级中,这三个条件同时满足后,系统就会自动对这张图片进行解码并显示。不过解码这个过程是在主线程里执行的,很耗CPU,因此如果在tableView或collectionView上显示了大量的图片,就势必会影响它们的滑动性能,那能否在子线程里对图片进行解码,然后把位图交给主线程去渲染呢?SDWebImage就做了这件事,我们只需要设置图片下载模块的一个属性即可shouldDecompressImages = YES,当然这个属性默认就是YES。

2、SDWebImage源码分析

你可能感兴趣的:(网络(七):SDWebImage源码浅析)