开源库学习 | SDWebImage

Cooler开源库学习系列之SDWebImage

问题1 如何设计图像的异步加载缓存机制 or SDWebImage是如何实现的

继续提问

问题2:能画出整体的框架流程图吗?

问题3:如何实现异步加载?

问题4:如何进行缓存?

问题5: 还需要考虑什么因素


以下分析的SDWebImage版本是3.7

SDWebImage 初印象

介绍

Features

  • Categories for UIImageView, UIButton, MKAnnotationView adding web image and cache management
  • An asynchronous image downloader
  • An asynchronous memory + disk image caching with automatic cache expiration handling
  • A background image decompression
  • A guarantee that the same URL won’t be downloaded several times
  • A guarantee that bogus URLs won’t be retried again and again
  • A guarantee that main thread will never be blocked
  • Performances!
  • Use GCD and ARC

粗体部分为基本功能,除此之外,我们还希望关注做了哪些性能优化

Supported Image Formats

  • Image formats supported by UIImage (JPEG, PNG, …), including GIF
  • WebP format, including animated WebP (use the WebP subspec)

GIF是4.0之后新增的功能,没有自己实现,依赖FLAnimatedImage

Requirements

  • iOS 7.0 or later
  • tvOS 9.0 or later
  • watchOS 2.0 or later
  • OS X 10.8 or later
  • Xcode 7.3 or later

使用方法

1
[imageView sd_setImageWithURL:[NSURL URLWithString:@"image_url"]];

引入UIImageView+WebCache.h文件后直接在ImageView调用sd_setImageWithURL方法就可以实现图片的异步下载。除了UIImageView之外,UIButton、UIImage也支持这个扩展

流程图

upload successful

通过流程图,可以大体看到WebCache的方法提供UIView的异步下载方法,SDWebImageManager 对下载进行管理,是从缓存获取(SDImageCache)还是进行下载(SDWebImageDownloader)

SDWebImage 深入剖析

源码分析

这里介绍源码实际调用流程,虽然我们经常用sd_setImageWithURL:placeholderImage方法,实际我们这个代理方法最终调用的都是UIView的这个扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad]; //Mark 1
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //Mark 2
if (!(options & SDWebImageDelayPlaceholder)) { //Mark 3
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
if (url) {
__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { // Mark 4
//Mark 5
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
// Mark 6
completedBlock(image, error, cacheType, url);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; //Mark 7
} else {
//URL 为空
}
}

我们就从这里开始,进行源码分析

调用流程

UIImageView+WebCache.h

Mark 1 主要是取消 Mark 7 处添加的Operation( UIView+WebCacheOperation.h),后面会详解。
Mark 2 就是为了保存当前的URL,通过sd_imageURL方法可以获取当前View的URL

问题6: 如果当前View调用多个不同的URL,是否出现覆盖情况?

Mark 3 主要是设置加载前的默认图。SDWebImageDelayPlaceholder是个位枚举类型,因此支持& | 的操作
支持11中不同类型的组合,详细见SDWebImageManager.h

Mark 4 是通过 SDWebImageManager的类方法创建一个operation,传入URL,options,progressBlock和completedBlock,这些参数是SDWebImageDowdloader需要的

Mark 5 是开始下载之后结束之后的回调,不管成功还是失败。但是如果operation取消了,那么这个回调将不再会调用

问题 7 : 如果继续执行会有什么问题?

如果相同的UIView进行多次回调,那么相当于对同一个Image进行多次写操作,存在资源竞争或者覆盖真正的数据,// See #699 for more details

1
2
3
4
5
6
7
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
if (weakOperation.isCancelled) {
// do Nothing
} else if (error) {
} else {
//OK
}

Mark 6 正常的回调Block,这里之所以列出来是想问一下,finish是表示是否完成,finish都是YES,那么什么情况是NO那,看了下代码,发现当URL为空,complete不为空时为NO。这个地方需要这个参数吗?[疑问脸]

UIView+WebCacheOperation.h

通过key关联图片下载的Operation,主要有3个函数:添加、取消、删除
Mark 7
如果有key先取消,在添加

1
2
3
4
5
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}

那么Operation是怎么执行起来的?看 Mark 4生成的Opetaion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
//省略 其他预处理
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
//如果是失败的URL,直接返回
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
//Error处理
return operation;
}
@synchronized (self.runningOperations) {// Mark 8
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url]; //Mark 9
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
// Mark 5处 直接返回,不会进行回调
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
//省略 options如果SDWebImageRefreshCached 并且 图片为nil,尝试重新下载,然后刷新
SDWebImageDownloaderOptions downloaderOptions = 0;
//opration赋值给downloaderOptions,只是在SDWebImageRefreshCached时处理SDWebImageDownloaderProgressiveDownload和SDWebImageDownloaderIgnoreCachedResponse情况。
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { //Mark 10
if (weakOperation.isCancelled) {
// Mark 5 问题 7 答案 什么也不做
}
else if (error) {
//Error处理,添加到failedURLs
}
else {
//省略了N中情况,都是储存,cacheOnDisk是表示是否需要存在硬盘上,然后调用compeleteBlock
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
//结束后再runningOperations 移除
if (finished) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:weakOperation];
}
};
}
}];
return operation;
}
  • SDWebImageManager.h
    这段代码省略了N中情况,但是主体的处理逻辑都是相似的。

Mark 8 runningOperations

1
2
3
4
@property (strong, nonatomic, readwrite) SDImageCache *imageCache;
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
@property (strong, nonatomic) NSMutableSet *failedURLs;
@property (strong, nonatomic) NSMutableArray *runningOperations;

总共定义了4个属性,SDImageCache负责缓存,SDWebImageDownloader负责下载,failedURLs是失败的URL,主要减少失败URL的请求,runningOperations是SDWebImageCombinedOperation类的可变数组,主要存储正在运行的operation,在对外暴露的cancelAll和isRunning方法都会用到。

Mark 9
通过cacheKeyForURL方法取出的key主要是用于缓存,这里之所以列出一是为了说cacheKeyFilter这个属性,可以缓存的URL进行处理(过滤,去除Query信息等)

1
2
3
4
5
6
7
8
9
10
11
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
if (!url) {
return @"";
}
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return url.absoluteString;
}
}

Mark 10
这里可以看出外部操作是SDWebImageCombinedOperation,这其实是服从SDWebImageOperation协议的NSObject子类,其实调用是内部的SubOperation相关下载和取消操作,之所以这样实现,是为了如果内部实现更换,对外的API可以不受影响

SDWebImageCombinedOperation

1
2
3
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;

实际进行下载操作是在SDWebImageDownloader.h类中

1
2
3
4
5
6
7
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
@property (weak, nonatomic) NSOperation *lastAddedOperation;
@property (assign, nonatomic) Class operationClass;
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks;
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders;
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;

这些属性都会在下面的方法中进行说明。

1
2
3
4
5
6
7
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
// Some Init Request and SDWebImageDownloaderOperation
}];
return operation;
}

这里调用addProgressCallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// Handle single download of simultaneous download request for the same URL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback();
}
});
}

代码解释的很清楚,从中可以找到下面问题的答案:请求多次相同URL如何处理?
URLCallbacks这个字典以URL为key,value是Dictionary,这里面包含了progressBlock和CompleteBlock,其中createCallback();只会在第一次调用,因为涉及到读写问题(读是在createCalbBack),因此需要barrier保证线程安全。

现在看createCallback中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
Init NSMutableURLRequest *request
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); // Mark 11
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
// Mark 12
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:progressBlock
completed:completeBlock
cancelled:cancellBlock}];
operation.shouldDecompressImages = wself.shouldDecompressImages;
if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; // Mark 13
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[wself.downloadQueue addOperation:operation]; //Mark 14
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { //Mark 15
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}

Mark 11

headersFilter 这里可以设置HTTP header fields, 可以进行公司指定域名过滤

Mark 12
创建一个SDWebImageDownloaderOperation,其中参数都是该类创建必要的参数,在progressBlock,completeBlock,cancellBlock都是通过Barrier取出callBackURL的dictionary,进行相应操作,看addProgressCallback: andCompletedBlock: forURL: createCallback:

Mark 13

NSURLCredential主要用于身份认证,通过userName和password也可以看出来,我个人用的比较少,以后在多看一下

Mark 14

这里添加到NSOperationQueue里,这时operation就会“SDWebImageDownloaderOperation”中的start方法,进行下载

Mark 15

这里通过依赖模拟实现了LIFO

最后在介绍最后一部分
>
SDWebImageDownloaderOperation.h, 就是通过NSURLConnection进行资源的下载,同时在不同的阶段,并且进行相应的通知,这里的代码,大家可以自行阅读,

细节

  • 使用SDWebImageManager,下载缓存。
1
2
3
4
5
6
7
8
9
10
11
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager loadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
// do something with image
}
}];

这对于我们自己封装UIImageView支持更丰富的默认图,点击加载等功能,提供了更加便捷的方法

  • 直接使用SDWebImageDownloader进行图片异步下载,而不仅仅局限于UIImageView的image赋值
1
2
3
4
5
6
7
8
9
10
11
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
[downloader downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
// do something with image
}
}];
  • 同样,我们可以使用SDImageCache进行图片的缓存
1
2
3
4
5
6
SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
// image is not nil if image was found
}];
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
  • cacheKeyFilter: 过滤query,可以在didFinishLaunchingWithOptions方法中添加,即在还没开始下载时进行设置。
    1
    2
    3
    4
    SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) {
    url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
    return [url absoluteString];
    };

SDWebImage各类之间的关系

经过上面分析,在整体看一下各个类之间的关系图

upload successful

写在最后

前面的问题的答案都已经在文章体现了,这里不作总结了,希望大家能有自己的理解总结一下。

这里稍微说一下还需要考虑的问题
1、image的类型, NSData+ImageContentType.h

2、为什么用NSCache, 不用NSDictionary

3、encode decode SDWebImageDecoder.h

至此,SDWebImage的源代码已经分析的差不多了,稍微粗糙一些,但是对于源码中需要注意的地方或者涉及的原因,结合自己的理解加以解释。如果文中有不正确的地方欢迎大家指正。