SDWebImage框架

研究SDWebImage框架.http://github.com/rs/SDWebImage是个支持异步下载与缓存的UIImageView扩展。
参考文档:http://cocoadocs.org/docsets/SDWebImage/4.0.0/

项目结构:

SDWebImageDownloader负责管理图片的下载队列;
SDWebImageDownloaderOperation负责真正的单一的图片下载请求;
SDImageCache负责图片的缓存(内存缓存和磁盘缓存);
SDWebImageManager是总的管理类,维护了一个SDWebImageDownloader实例和一个SDImageCache实例,是下载与缓存的桥梁;
SDWebImageDecoder负责图片的解压缩;
SDWebImagePrefetcher负责图片的预取;
UIImageView+WebCache和其他的扩展都是与用户直接打交道的。

项目整体架构:

UIImageView+WebCache和UIButton+WebCache直接为表层的 UIKit框架提供接口, 而 SDWebImageManger负责处理和协调SDWebImageDownloader和SDWebImageCache, 并与 UIKit层进行交互。SDWebImageDownloaderOperation真正执行下载请求;最底层的两个类为高层抽象提供支持。

UIImageView+WebCache

集成SDWebImage异步下载和使用的UIImageView远程图像缓存。
最常使用的方法:

1
2
3
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

使用栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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];
}

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://example.com/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder"]];

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

常用的场景是已知图片的url地址,来下载图片并设置到UIImageView上。UIImageView+WebCache提供了一系列的接口:

1
2
3
4
5
6
- (void)setImageWithURL:(NSURL *)url;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)setImageWithURL:(NSURL *)url completed:(SDWebImageCompletedBlock)completedBlock;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletedBlock)completedBlock;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletedBlock)completedBlock;

这些接口最终会调用

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

方法的第一行代码[self sd_cancelCurrentImageLoad]是取消UIImageView上当前正在进行的异步下载,确保每个 UIImageView 对象中永远只存在一个 operation,当前只允许一个图片网络请求,该 operation 负责从缓存中获取 image 或者是重新下载 image。具体执行代码是:UIView+WebCacheOperation中
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key的方法,实际上,所有操作都是由一个operationDictionary字典维护的,执行新的操作之前,先cancel所有的operation。这里的cancel是SDWebImageOperation协议里面定义的。

1
2
3
4
5
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}

是一种占位图策略,作为图片下载完成之前的替代图片。dispatch_main_async_safe是一个宏,保证在主线程安全执行.

然后判断url,url为空就直接调用完成回调,报告错误信息;
url不为空,用SDWebImageManager单例sharedManager的方法:

1
2
3
4
5
// SDWebImageManager是将UIImageView+WebCache同SDImageCache链接起来的
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock

上面方法下载图片.下载完成后刷新UIImageView的图片.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//图像的绘制只能在主线程完成
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (!wself) return;
dispatch_main_sync_safe(^{
__strong UIButton *sself = wself;
if (!sself) return;
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
[sself setImage:image forState:state];
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];

最后,把返回的id operation添加到operationDictionary中,方便后续的cancel。(UIView+WebCacheOperation方法中的)
[self sd_setImageLoadOperation:operation forKey:@”UIImageViewImageLoad”];

UIView+WebCacheOperation

方法:
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key;
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key;
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key;
具体的实现是使用runtime的objc_associate方法给UIView绑定了一个属性,这个属性的key是static char loadOperationKey的地址,
这个属性是NSMutableDictionary类型,value为操作,key是针对不同类型的视图和不同类型的操作设定的字符串。这个key值是用来存储和识别队列的。

为什么要使用static char loadOperationKey的地址作为属性的key,实际上很多第三方框架在给类绑定属性的时候都会使用这种方案(如AFN),这样做有以下几个好处:

1.占用空间小,只有一个字节。
2.静态变量,地址不会改变,使用地址作为key总是唯一的且不变的。
3.避免和其他框架定义的key重复,或者其他key将其覆盖的情况。比如在其他文件(仍然是UIView的分类)中定义了同名同值的key,使用objc_setAssociatedObject进行设置绑定的属性的时候,可能会将在别的文件中设置的属性值覆盖。

SDWebImageManager

SDWebImageManger 负责处理和协调 SDWebImageDownloader 和 SDWebImageCache. 并与 UIKit 层进行交互, 而底层的一些类为更高层级的抽象提供支持.
SDWebImageManager.h首先定义了一些枚举类型的SDWebImageOptions。参考http://www.jianshu.com/p/6ae6f99b6c4c#

然后声明了三个Block:
//操作完成的回调,被上层的扩展调用。 typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);
//被SDWebImageManager调用。如果使用了SDWebImageProgressiveDownload标记,这个block可能会被重复调用,直到图片完全下载结束,finished=true,再最后调用一次这个block。 typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);
//SDWebImageManager每次把URL转换为cache key的时候调用,可以删除一些image URL中的动态部分。 typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);

定义了SDWebImageManagerDelegate协议:

1
2
3
4
5
6
7
8
9
10
11
12
@protocol SDWebImageManagerDelegate <NSObject>
@optional
/*
*主要作用是当缓存里没有发现某张图片的缓存时,是否选择下载这张图片(默认是yes),可以选择no,那么sdwebimage在缓存中没有找到这张图片的时候不会选择下载
*/
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
/**
*在图片下载完成并且还没有加入磁盘缓存或者内存缓存的时候就transform这个图片.这个方法是在异步线程执行的,防治阻塞主线程.
*至于为什么在异步执行很简单,对一张图片纠正方向(也就是transform)是很耗资源的,一张2M大小的图片纠正方向你可以用instrument测试一下耗时.很恐怖
*/
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
@end

SDWebImageManager是单例使用的,分别维护了一个SDImageCache实例和一个SDWebImageDownloader实例。

@property (strong, nonatomic, readonly) SDImageCache *imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;

再有一个Block
@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;这个Block的作用是需要把一个URL转换成一个cache key,这个能被用来移除掉图片URL的一部分。

方法:

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
//初始化SDWebImageManager单例
+ (SDWebImageManager *)sharedManager;
//下载图片
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
//缓存给定URL的图片
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
//取消当前所有的操作
- (void)cancelAll;
//监测当前是否有进行中的操作
- (BOOL)isRunning;
//监测图片是否在缓存中, 先在memory cache里面找 再到disk cache里面找
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
//监测图片是否缓存在disk里
- (BOOL)diskImageExistsForURL:(NSURL *)url;
//监测图片是否在缓存中,监测结束后调用completionBlock,仍在主队列
- (void)cachedImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//监测图片是否缓存在disk里,监测结束后调用completionBlock
- (void)diskImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//返回给定URL的cache key
- (NSString *)cacheKeyForURL:(NSURL *)url;

我们主要研究第二个:下载图片
首先,判断 url 的合法性。
第一个判断条件是防止很多用户直接传递NSString作为NSURL导致的错误,第二个判断条件防止crash。

1
2
3
4
5
6
7
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}

再新建了SDWebImageCombinedOperation

1
2
3
4
5
6
7
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}

集合failedURLs保存之前失败的urls,如果url为空或者url之前失败过且不采用重试策略,直接调用completedBlock返回错误。

1
2
3
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}

runningOperations是一个可变数组,保存所有的operation,主要用来监测是否有operation在执行,即判断running 状态。

1
2
3
4
5
6
7
8
9
10
// 根据 URL 生成对应的 key,没有特殊处理为 [url absoluteString];
NSString *key = [self cacheKeyForURL:url];
// 去缓存中查找图片(参见 SDImageCache)先在memory以及disk的cache中查找是否下载过相同的照片
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}

如果在缓存中找到图片,直接调用completedBlock,第一个参数是缓存的image。completedBlock(image, nil, cacheType, YES, url);
下面方法:

1
2
3
4
5
6
7
8
9
10
11
12
if (image) {
// 在缓存中找到图片了,直接返回
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}

如果没有在缓存中找到图片,或者不管是否找到图片,只要operation有SDWebImageRefreshCached标记,那么若SDWebImageManagerDelegate的shouldDownloadImageForURL方法返回true,即允许下载时,都使用 imageDownloader 的(SDWebImageDownloader)

1
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock

方法下载。如果操作队列取消则什么都不做,若发生错误,则直接调用completedBlock返回错误,并且视情况将url添加到failedURLs里面;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 if (error) {
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});

if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCanc
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}

若下载成功:

若支持失败重试,将url从failURLs里删除:

1
2
3
4
5
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}

如果delegate实现了imageManager:transformDownloadedImage:withURL:的方法,图片在缓存之前,需要做转换(在全局队列中调用,不阻塞主线程)。转化成功切下载全部结束,图片存入缓存,调用completedBlock回调,第一个参数是转换后的image。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {                        
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
}

否则:直接存入缓存(SDImageCache中的
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk方法),调用completedBlock回调,第一个参数是下载的原始image。

1
2
3
4
5
6
7
8
9
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}

dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});

最后一种情况是:图片不在缓存中也不允许下载,直接调用completedBlock,第一个参数为nil。

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !weakOperation.isCancelled) {//为啥这里用weakOperation TODO
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});

//最后都要将这个operation从runningOperations里删除。

@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}

Downloader

SDWebImageDownloader

Asynchronous downloader dedicated and optimized for image loading.专用的并且优化的图片异步下载器.
这个类的核心功能就是下载图片.
定义了枚举类型:SDWebImageDownloaderOptions下载的选项和SDWebImageDownloaderExecutionOrder执行顺序,

 //默认的下载顺序,先进先出
SDWebImageDownloaderFIFOExecutionOrder,
//后进先出
SDWebImageDownloaderLIFOExecutionOrder

开始下载和结束下载的通知的extern NSString *const的全局变量定义。
三个Block:

1
2
3
4
5
6
// 下载进度回调(返回已经接收的图片数据的大小,未接收的图片数据的大小)
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
// 下载完成回调,返回图片数据或错误
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
//过滤HTTP请求的Header
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);

属性:
是否应该压缩图片,最大并发下载数,当前下载量,下载队列的时长(默认15s),下载队列执行顺序,对于请求队列设置默认的URL证书,设置用户名,设置密码,设置过滤HTTP请求的header。
类方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//给每个HTTP下载请求头的指定field设置值。
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
//返回HTTP特定field的值
- (NSString *)valueForHTTPHeaderField:(NSString *)field;
//设置一个SDWebImageDownloaderOperation的子类作为下载请求的默认NSOperation
- (void)setOperationClass:(Class)operationClass;
//创建一个SDWebImageDownloader异步下载实例,图片下载完成或错误时,通知delegate回调。方法返回一个 SDWebImageOperation
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
// 设置下载队列为挂起状态
- (void)setSuspended:(BOOL)suspended;
//取消队列中的所有操作。
- (void)cancelAllDownloads;

SDWebImageDownloader 下载管理器是一个单例类,它主要负责图片的下载操作的管理。图片的下载是放在一个 NSOperationQueue 操作队列中来完成的,@property (strong, nonatomic) NSOperationQueue *downloadQueue;默认最大的并行操作个数是6。队列中每一个SDWebImageDownloaderOperation实例才是真正的下载请求执行者。
我们重点研究核心下载方法

1
2
3
4
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

这个方法实际上就是调用了另外一个关键方法:

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
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock 
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(NSURL *)url
createCallback:(SDWebImageNoParamsBlock)createCallback {
// url作为URLCallbacks的key,如果为nil ,直接调用completedBlock。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
//将所有下载任务的网络响应处理放到barrierQueue队列中。
//并设置栅栏来确保同一时间只有一个线程操作URLCallbacks属性
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
//修改url对应的URLCallbacks
//URLCallbacks是一个字典: key是url, value是数组
//数组的元素是字典,key是callback类型字符串,value是callback的block
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;
//第一次请求这个url 才去真正做http请求
if (first) {
createCallback();
}
});
}

该方法为下载的操作添加回调的块, 在下载进行时, 或者在下载结束时执行一些操作。图片下载的progressBlock和completedBlock回调由一个字典URLCallbacks管理。字典的key是图片的url,value 是一个数组,数组只包含一个元素,这个元素的类型是NSMutableDictionary类型,这个字典的key为NSString类型代表着回调类型,value为block,是对应的回调。由于允许多个图片同时下载,因此可能会有多个线程同时操作URLCallbacks属性。为了保证线程安全,将下载操作作为一个个任务放到barrierQueue队列中,并设置栅栏来确保同一时间只有一个线程操作URLCallbacks属性
两个回调对应的key分别是

1
2
static NSString *const kProgressCallbackKey = @"progress";
static NSString *const kCompletedCallbackKey = @"completed";

//为了去阻止潜在的重复的缓存(NSURLCache和SDWebCache缓存),如果有一种缓存我们不能去请求缓存

1
2
3
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;

如果URLCallbacks没有url这个key,说明是第一次请求这个url,需要调用createCallback创建下载任务,即使用

1
2
3
4
5
6
- (id)initWithRequest:(NSURLRequest *)request
inSession:(NSURLSession *)session
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock

调用这个方法后的progressBlock是:对已经接收到的大小和期待的大小调用callback;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
//异步提交, 当前线程直接返回
//callbacks在main_queue中并行执行
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback)
callback(receivedSize, expectedSize);
});
}

completedBlock是,对image和data调用callback,在completed block中我们取出存储在URLCallbacks中的completedBlock.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback)
callback(image, data, error, finished);
}

cancelBlock是:我们移除存储在URLCallbacks的数组。

1
2
3
4
5
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});

接下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//是否解压下载的图片,默认是YES,但是会消耗掉很多内存,如果遇到内存不足的crash时,将值设为NO。
operation.shouldDecompressImages = wself.shouldDecompressImages;
//设置证书
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
//设置队列优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//加入操作队列后, operation 真正开始执行start
//所有的下载任务放在downloadQueue队列中.
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {

//若果是LIFO的下载执行顺序,还要加上任务的依赖,也就是说依赖的任务都完成后,才能执行当前任务
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];

SDWebImageDownloaderOperation

SDWebImageDownloaderOperation : NSOperation 是NSOperation的子类,遵循SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate协议,并重写了start方法。在start方法中真正处理HTTP请求和URL链接。
看一下start方法:首先检测下载状态:

1
2
3
4
5
6
//管理下载状态,如果已取消,则重置当前下载并设置完成状态为YES
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}

如果是iOS4.0以上的版本,还需要考虑是否在后台执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
//如果设置了在后台执行,则进行后台执行
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
// 如果在系统规定时间内任务还没有完成(一般是10分钟),结束后台任务
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];

Version3.8中,下载已经由原先的NSURLConnection切换到了NSURLSession了:
创建好任务后开始执行请求。 如果任务创建成功,可能需要调用progressBlock回调并发送下载开始的通知;如果创建失败,直接执行完成回调,并传递一个connection没有初始化的错误:

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
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
//为任务创建会话,我们给delegateQueue设置nil来创建一个顺序操作队列去执行所有的代理方法和完成回调。
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
//开启任务
[self.dataTask resume];
if (self.dataTask) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
dispatch_async(dispatch_get_main_queue(), ^{
// 在主线程中发送开始下载的通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
} else {
//如果session创建失败,直接执行完成回调,并传递一个connection没有初始化的错误
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}

任务开始后,我们需要关注NSURLSessionDataDelegate的几个代理方法。另外还有NSURLSessionTaskDelegate的两个代理方法:

NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
把URL作为Key值,

SDImageCache

SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed asynchronous so it doesn’t add unnecessary latency to the UI.
SDImageCache维持了一个内存缓存memCache和一个可选的磁盘缓存。同时,磁盘缓存的写操作是异步的,所以它不会对 UI 造成不必要的影响。

内存缓存是用NSCache实现的,以Key-Value的形式存储图片,当内存不够的时候会清除所有缓存图片。
磁盘缓存则是缓存到沙盒中,文件替换方式是以时间为单位,剔除时间大于一周的图片文件。默认情况下,会将缓存保存在应用沙盒的cache/com.hackemist.SDWebImageCache.default中.

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

枚举:缓存主要有不缓存,磁盘缓存、内存缓存。
三个BLock:请求完成,检查缓存完成,计算大小。
属性:是否压缩图片(遇到崩溃因过度的内存消耗将它设置为NO),关闭iCloud云备份[默认为YES],使用内存缓存[默认为YES],最大内存成本,最大内存数量限制,最大内存周期,最大内存大小。
方法:实例化,内存存储的使用空间,创建磁盘缓存的目录,添加只读缓存路径,添加只读路径,用key来存储图片到内存或磁盘缓存,用key存储图片到内存或可选的磁盘,同步查询内存\磁盘缓存,同步从内存和磁盘中移除图片,异步从内存和磁盘中移除图片,清理所有内存缓存图片,清理所有磁盘缓存图片,移除所有过期的缓存图片,通过磁盘缓存获得使用大小,获得磁盘缓存中图片的数量,异步计算磁盘缓存的大小,如果图片已经存在磁盘缓存中同步检查,检查如果图片已经在磁盘内存中存在则不要下载,对于特定的key获得缓存路径,

重要方法:

1
2
3
4
5
6
7
8
//将key对应的image存储到内存缓存和磁盘缓存中
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
//将key对应的image存储到内存缓存,是否同时存入磁盘中由参数toDisk决定
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
//功能同上,参数recalculate指明imageData是否可用或者应该从UIImage重新构造;参数imageData是由服务器返回,可以用于磁盘存储,这样可以避免将image转换为一个可存储/压缩的图片以节省CPU。
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
//真正将key对应的image存储到磁盘缓存中
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key;

第一个方法和第二个方法最终都会调用第三个方法,

如果需要存储到memory cache中,首先存入memcache。

1
2
3
4
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}

如果需要存储到disk cache,在子线程中串行存储到disk cache中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (toDisk) {
dispatch_async(self.ioQueue, ^{
//串行队列io队列
NSData *data = imageData;

if (!data && image) {
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
data = [image sd_imageDataAsFormat:imageFormatFromData];
}

[self storeImageDataToDisk:data forKey:key];
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
  1. disk cache的文件名是key做MD5后的字符串:
#pragma mark SDImageCache (private)

- (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];

    return filename;
}

面试题

  1. 过程
    1>UIImageView+WebCache: setImageWithURL:placeholderImage:options: 先显示 placeholderImage ,同时由SDWebImageManager 根据 URL 来在本地查找图片。

2>SDWebImageManager: downloadWithURL:delegate:options:userInfo: SDWebImageManager是将UIImageView+WebCache同SDImageCache链接起来的类, SDImageCache: queryDiskCacheForKey:delegate:userInfo:用来从缓存根据CacheKey查找图片是否已经在缓存中

3>如果内存中已经有图片缓存, SDWebImageManager会回调SDImageCacheDelegate : imageCache:didFindImage:forKey:userInfo:

4>而 UIImageView+WebCache 则回调SDWebImageManagerDelegate: webImageManager:didFinishWithImage:来显示图片。

5>如果内存中没有图片缓存,那么生成 NSInvocationOperation 添加到队列,从硬盘查找图片是否已被下载缓存。

6>根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

7>如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

8>如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

9>共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

10>图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
11>connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
12>connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
13>图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
14>在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。
15>imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
16>通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
17>将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。
18>写文件到硬盘在单独 NSInvocationOperation 中完成,避免拖慢主线程。
19>如果是在iOS上运行,SDImageCache 在初始化的时候会注册notification 到 UIApplicationDidReceiveMemoryWarningNotification 以及 UIApplicationWillTerminateNotification,在内存警告的时候清理内存图片缓存,应用结束的时候清理过期图片。
20>SDWebImagePrefetcher 可以预先下载图片,方便后续使用

  1. SDWebImage的缓存策略,是如何从缓存中hit一张图片的;使用了几级缓存;缓存如何满了如何处理,是否要设置过期时间;
    1. 设计一个网络图片缓存器(SDWebImage实现原理必需要了解)
文章目录
  1. 1. UIImageView+WebCache
  2. 2. UIView+WebCacheOperation
  3. 3. SDWebImageManager
  4. 4. Downloader
    1. 4.1. SDWebImageDownloader
    2. 4.2. SDWebImageDownloaderOperation
  5. 5. SDImageCache
  • 面试题
  • 本站总访问量 本站访客数人次 ,