您的位置:首页 > 产品设计 > UI/UE

SDWebImage 源码阅读笔记(一)

2016-11-14 17:34 603 查看


简介

Asynchronous image downloader with cache support as a UIImageView category.

言简意赅:SDWebImage 以 UIImageView category(分类)的形式,来支持图片的异步下载与缓存。

其提供了以下功能:

以 UIImageView 的分类,来支持网络图片的加载与缓存管理
一个异步的图片加载器
一个异步的内存 + 磁盘图片缓存
支持 GIF
支持 WebP
后台图片解压缩处理
确保同一个 URL 的图片不被多次下载
确保虚假的 URL 不会被反复加载
确保下载及缓存时,主线程不被阻塞
使用 GCD 与 ARC
支持 Arm64

UIImageView+WebCache

首先,SDWebImage 最常见的使用场景想必如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

#import <SDWebImage/UIImageView+WebCache.h>

...

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}

// 在这里,我们使用 UIImageView 分类提供的 sd_setImageWithURL: 方法来加载网络图片
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

cell.textLabel.text = @"My Text";
return cell;
}

我们在使用 UITableView 时,往往需要在 Cell 上显示来自网络的图片,这里最关键的一行代码便是:

1
2

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

于是我们「CMD + 左键」来到了
UIImageView+WebCache
查看具体的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
202122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
43
44
45
46
47
48
49
50
5152
53
54
55
56

/**
* 根据 url、placeholder 与 custom options 为 imageview 设置 image
*
* 下载是异步的,并且被缓存的
*
* @param url            网络图片的 url 地址
* @param placeholder    用于预显示的图片
* @param options        一些定制化选项
* @param progressBlock  下载时的 Block,其定义为:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
* @param completedBlock 下载完成时的 Block,其定义为:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
*/
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

if (!(options & SDWebImageDelayPlaceholder)) {
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) {
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);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
dispatch_main_async_safe(^{
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
if (completedBlock) {
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}

虽然代码只有几十行,但其中涉及到的知识点却可不少哦,不要急,让我们将迷雾一层层剥开:

UIView+WebCacheOperation

首先来看:

1

[self sd_cancelCurrentImageLoad];

「CMD + 左键」后带我们来到了
UIView+WebCacheOperation


1
23
4
5
6
7
8
9
10
1112
13
14
15
16
17

- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// 取消正在进行的下载队列
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}

框架中的所有操作实际上都是通过一个 operationDictionary(具体查看 UIView+WebCacheOperation)来管理的,而这个 Dictionary 实际上是通过动态的方式(详情可参见:Objective-C
Associated Objects 的实现原理)添加到 UIView 上的一个属性,至于为什么添加到 UIView 上, 主要是因为这个 operationDictionary 需要在 UIButton 和 UIImageView 上重用,所以需要添加到它们的根类上。

当执行
sd_setImageWithURL:
函数时,首先会 cancel 掉 operationDictionary 中已经存在的 operation,并重新创建一个新的 SDWebImageCombinedOperation 对象来获取 image,该 operation 会被存入 operationDictionary 中。

这样来保证每个 UIImageView 对象中永远只存在一个 operation,当前只允许一个图片网络请求,该 operation 负责从缓存中获取 image 或者是重新下载 image。

SDWebImageCombinedOperation
的 cancel 操作同时会 cacel 掉缓存查询的 operation 以及 downloader 的 operation


dispatch_main_sync_safe & dispatch_main_async_safe 宏定义

再来看:

1
23

dispatch_main_async_safe(^{
self.image = placeholder;
});

上述代码中的
dispatch_main_sync_safe
dispatch_main_async_safe
均为宏定义, 点进去一看发现宏是这样定义的:

1
23
4
5
6
7
8
9
10
1112
13

#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}

#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

相信你通过这两个宏的名字就能猜到它们的作用了: 因为图像的绘制只能在主线程完成,所以
dispatch_main_sync_safe

dispatch_main_async_safe
就是为了保证 block 能在主线程中执行。

SDWebImageManager

SDWebImageManager.h
中你可以看到关于 SDWebImageManager 的描述:

The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading
with caching in another context than a UIView.

这个类就是隐藏在
UIImageView+WebCache
背后,用于处理异步下载和图片缓存的类,当然你也可以直接使用 SDWebImageManager 的上述方法
downloadImageWithURL:options:progress:completed:
来直接下载图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
202122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
43
44
45
46
47
48
49
50
5152
53
54
55
56
57
58
59
60

/**
* 如果在缓存中则直接返回,否则根据所给的 URL 下载图片
*
* @param url            网络图片的 url 地址
* @param options        一些定制化选项
* @param progressBlock  下载时的 Block,其定义为:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
* @param completedBlock 下载完成时的 Block,其定义为:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
* @return 				 返回 SDWebImageOperation 的实例
*/
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
/**
* 前面省略 n 行,主要作了如下处理:
* 1. 判断 url 的合法性
* 2. 创建 SDWebImageCombinedOperation 对象
* 3. 查看 url 是否是之前下载失败过的
* 4. 如果 url 为 nil,或者在不可重试的情况下是一个下载失败过的 url,则直接返回操作对象并调用完成回调
*/
// 根据 URL 生成对应的 key,没有特殊处理为 [url absoluteString];
NSString *key = [self cacheKeyForURL:url];
// 去缓存中查找图片(参见 SDImageCache)
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType)
{
/* ... */
// 如果在缓存中没有找到图片,或者采用的 SDWebImageRefreshCached 选项,则从网络下载
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
dispatch_main_sync_safe(^{
// 如果图片找到了,但是采用的 SDWebImageRefreshCached 选项,通知获取到了图片,并再次从网络下载,使 NSURLCache 重新刷新
completedBlock(image, nil, cacheType, YES, url);
});
}
/* 下载选项设置 */
// 使用 imageDownloader 开启网络下载
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
/* ... */
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);
}
});
}
}
/* ... */
}
else if (image) {
// 在缓存中找到图片了,直接返回
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
}
}];
return operation;
}

更详细的注解可参见:SDWebImage源码解析之SDWebImageManager的注解

要点

在 SDWebImageManager 中管理了一个 failedURLs 的 NSMutableSet,里面下载失败的 url 会被存储下来。同时,可以通过 SDWebImageRetryFailed 来强制继续重试下载

查找缓存,若缓存中没有 image 则通过 SDWebImageDownloader 来进行下载,下载完成后通过 SDImageCache 进行缓存,会同时缓存到 memCache 和 diskCache 中

可以看到 SDWebImageManager 这个类的主要作用就是为 UIImageView+WebCache 和 SDWebImageDownloader,SDImageCache 之间构建一个桥梁,使它们能够更好的协同工作,在接下来的系列文章中,就让我们一探究竟:它是如何协调异步下载和图片缓存的?

原文:http://itangqi.me/2016/03/19/the-notes-of-learning-sdwebimage-one/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  uiimageview