简析SDWebImage

前言

SDWebImage是一个强大的第三方图片异步加载库,从事iOS开发的人或多或少用过它。今天我点开源码一看,里面一些实现细节确实令人玩味,同时也让我了解到缓存机制是如何实现。

简析

1
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;

作者利用category为一些UIKit类拓展出相应的缓存功能,其中UIImageView和UIButton使用频率比较高,因为日常开发中,我们需要这两个控件加载图片。上面那个方法是UIImageView+WebCache最常用的方法,传一个url,UIImageView就会将网络图片加载出来,这个方法看起来简单,可是背后的实现没那么容易,不禁感慨大神之牛,巧妙的设计和背后复杂的实现竟然浓缩成这样的一个接口函数。

点进去看源码

1
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];

这个是原始接口

点进去这个原始接口

我可以看到这个函数体

1
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock

这个函数体就是就是sd_setImage的实现逻辑,利用block回调,有加载过程的回调和加载完成的回调

1
2
3
4
5
6
7
8
[self sd_cancelCurrentImageLoad];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }

首先,它先取消下载队列里正在进行的任务,将图片的url和UIImageView关联起来作为UIImageView的一部分;如果有placeholder,就先将它显示出来;

接着就是网络加载图片实现逻辑的主要部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
}];

利用一个全局的SDWebImageManager来进行下载缓存操作的管理和调度。SDWebImageManager由两部分构成,由SDImageCache缓存器和SDWebImageDownloader下载器构成。同步上面加载过程的回调,在completed里除了同步加载完成的回调外,还将block回调的image加载在控件上面。

1
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

这个函数是为了判断当前是否有重复的网络图片加载操作,如果有,就取消,key作为判断加载操作的标识。

点进去看downloadImageWithURL源码

1
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];

首先,先new一个新的operation,operation是遵守SDWebImageOperation协议的一个对象,协议有个cancle方法,利用cancle可以取消当前操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@synchronized (self.failedURLs) {
    isFailedUrl = [self.failedURLs containsObject:url];
}

if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
    dispatch_main_sync_safe(^{
        NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
        completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
    });
    return operation;
}
@synchronized (self.runningOperations) {
    [self.runningOperations addObject:operation];
}

接着我们可以看到一个实现的亮点,它用一个failedURLs集合来记录失败或者无效的url,如果传进来的url是failedURLs这个集合里的或者是为空,则完成操作并回调,否则将operation添加到运行的operation数组中。

1
2
 NSString *key = [self cacheKeyForURL:url];
    }

它将url缓存起来,给他一个key值

1
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {}];

SDWebImageManager的缓存器根据缓存的url的key值来寻找是否有对应的缓存

点进去看queryDiskCacheForKey源码

1
2
3
4
if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

如果相关的缓存记录则doneBlock回调,标识为SDImageCacheTypeNone

1
2
3
4
5
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

先在内存里寻找,SDImage用的内存缓存是用NSCache实现的,如果有就是doneBlock回调,标志为SDImageCacheTypeMemory

1
2
3
4
5
6
7
8
9
  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);
            });

如果没有,就从磁盘缓存里找,如果找到了,就将他缓存到内存中,doneBlock回调,标志为SDImageCacheTypeDisk

返回上层函数查看done:^(UIImage *image, SDImageCacheType cacheType) {}这个block的主体

我们只看如果没有图片缓存该如何做

1
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {};

SDWebImageManager的下载器开启下载operation来下载图片

点进去看downloadImageWithURL源码

下载的实现就不展开讲了,它利用的苹果提供的NSURLConnection来实现。

1
wself.lastAddedOperation = operation;

同时记录最后一个下载操作,最后block回调。 图片下载完成后,利用SDWebImageManager的缓冲器将图片缓存到内存和磁盘中,并将operation从运行的operation数组移除,如果失败则记录他是一个失败的url,成功则block回调。

Comments