简单的介绍
SDWebImage 提供图片的异步下载和缓存,对外通过 categories 封装 UIImageView
,UIButton
,MKAnnotationView
的接口供使用者使用。
SDWebImage 的磁盘缓存有对有效期和最大容量的限制处理,内存缓存在系统报内存警告的时候会清除。它还保障几个相同的 URL 不会被重复下载,不可用的 URL 不会一次次的被重试,主线程绝不会被阻塞。
直观的感受
SDWebImage 的接口简单易用,开发者一句代码就能使用,代码: - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
,复杂的逻辑和实现都被隐藏在这句代码的后面。
各模块独立,Cache缓存模块提供库的内存缓存和可选的磁盘缓存支持,Downloader下载模块提供基于 NSURLSession
和 NSOperation
的下载器。SDWebImageManager
则将 Cache 和 Downloader 两个模块很好的整合在一起,提供基于缓存的图片加载功能。最后是使用 categories 封装了常用 UI 控件的接口。
GitHub 上拥有万的 star,无数的使用者,在项目中久经考验,是程序猿学习的好材料。
从‘头’开始
这个头就是最常用的类 UIImageView+WebCache
,类里面暴露了很多加载图片的接口:
- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
...
但是归根到底都是调用的这句,我们来看这里面的代码。
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
代码的第一句是
[self sd_cancelCurrentImageLoad];
看方法名就知道它的作用,就是取消这个视图 ImageView 正在加载图片的操作,如果这个 ImageView 正在加载图片,保障在开始新的加载图片任务之前,取消掉正在进行的加载操作。
看下具体的实现代码 UIImageView+WebCache
:
- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
- (void)sd_cancelCurrentAnimationImagesLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewAnimationImages"];
}
两个 key 说明有两个不一样的加载方式,一个是单张图片的,另一个是连续下载多张,放到 NSArray<UIImage *> *animationImages
中。
看下取消操作的代码实现,UIView (WebCacheOperation)
:
- (NSMutableDictionary *)operationDictionary {
NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
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];
}
}
代码中,通过 objc_setAssociatedObject
关联对象的方法,给 UIImageView 动态添加了一个 NSMutableDictionary
的属性。通过 key-value 维护这个 ImageView 已经有了哪些下载操作,如果是数组就是 UIImageViewAnimationImages
否则就是 UIImageViewImageLoad
。最后获得的都是遵从了 <SDWebImageOperation>
协议的对象,可以统一调用定义好的方法 cancel,达到取消下载操作的目的,如果 operation 都被取消了,则删除对应 key 的值。
继续看 - (void)sd_setImageWithURL: placeholderImage: options: ;
里的代码
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
如果加载图片的选项不是 SDWebImageDelayPlaceholder
则会在主线程中先设置 placeholder 的占位图,
SDWebImageDelayPlaceholder
的情况后面说。dispatch_main_async_safe
是一个宏定义,我们可以参考这种写法。
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}
看起来很简洁。
下面这段就是这个类中比较关键的代码了,
// check if activityView is enabled or not
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
}
__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) {
[wself removeActivityIndicator];
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"];
先检查 activityView 是否可用,可用的话给 ImageView 正中间添加一个活动指示器,并旋转,加载图片完成或失败都会清除掉。__weak __typeof(self)wself = self;
避免循环引用,接下来就是调用 SDWebImageManager
的方法 downloadImageWithURL: options: progress: completed:
,在该方法的 completed block 回调中,如果 option 是 SDWebImageAvoidAutoSetImage
,就是要求不要给 ImageView 自动设置图片,则只回调 completedBlock 然后 return,否则有 image 就设置给 ImageView 。没有 image 通常就是错误情况,如果 option 是 SDWebImageDelayPlaceholder
则设置占位图(可以设置成提示用户图片没加载出来的图片),最后回调 completedBlock。上面代码最后一句是把这个 operation 存到 ImageView 的 NSMutableDictionary
中,为了之前提到的 [self sd_cancelCurrentImageLoad];
操作准备的。
UIImageView+WebCache
类中的代码还是很容易的,逻辑很清晰,没有难懂的地方。接下来我们去看 SDWebImageManager
的核心方法:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
Cache 和 Downloader 的管理者 SDWebImageManager
我们看这个方法前几句:
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
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;
}
注释中说 NSString
替换 NSURL
做为参数传进来是很常见的错误,但奇怪的是 XCode 没有类型错误的警告。这个我试过是有警告提示的,可能更早一些的 XCode 版本是这样的。这几句就是参数的校验,没什么好说的。
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
这个类的作用是管理多个模块的取消操作,具体是怎么实现的,后面的代码会提到,__weak
修饰是为了防止循环引用。
下面就是文章最开始提到的功能之一,不可用的 URL 不会一次次重试的功能实现:
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
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;
}
在 downloader 的下载方法的 completedBlock 中会将下载失败的 URL ,维护到 Set 集合中(黑名单),代码会在后面提到。这段代码的意思是如果发现 url 长度为 0 ,或者是下载失败过的 url ,且没有要求重试则直接创建 NSError
并 回调 completedBlock 。
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
manager 维护了一个数组 self.runningOperations
,将所有操作放进去,便于管理。(比如统一调用 cancel )
下面是比较核心的代码:
NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {...}
通过 url 获取用来缓存的 key,尝试去缓存中取图片。queryDiskCacheForKey: done:
方法返回了一个 NSOperation
对象并赋值给了 SDWebImageCombinedOperation
的 cacheOperation
,这个类(SDWebImageCombinedOperation)中还有一个属性 cancelBlock
也会包括一些取消操作。它还实现了协议 <SDWebImageOperation>
,这个协议里只需要实现一个方法,就是 - (void)cancel;
。
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
而 SDWebImageCombinedOperation
的实现类中,实现了这个 cancel 方法,并调用了这些取消操作。
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
// TODO: this is a temporary fix to #809.
// Until we can figure the exact cause of the crash, going with the ivar instead of the setter
// self.cancelBlock = nil;
_cancelBlock = nil;
}
}
这样,调用者只要调用 operation 的 cancel()
,就可以统一对多个模块类做取消操作。
然后看下查询缓存的方法 - (NSOperation *)queryDiskCacheForKey: done:
的实现代码:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return 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;
}
会先到内存缓存中去查找,如果命中则直接回调,没有命中继续在磁盘缓存中查找。查找任务是异步的,在一个串行队列中,先生成一个 NSOperation
对象返回,也就是赋值给了 operation.cacheOperation
。在查找任务中,会先检测这个 operation 有没有被取消,如果取消则直接 return,这就实现了 SDWebImageCombinedOperation
可以取消查找缓存的操作。之后的代码就是去磁盘缓存中查找图片,且如果需要内存缓存就存进去,最后回调 doneBlock。这段代码被放入到了 @autoreleasepool 中包裹起来,是因为查找出来的图片可能会比较大,占用较多的内存,保障能够及时的回收它。关于 @releasepool 的原理参考这篇文章。
现在回到 SDWebImageManager
中继续看,在 queryDiskCacheForKey:
的 doneBlock 中,
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
如果操作被取消,则删除掉 self.runningOperations
的操作,然后 return。
接下来会有三个条件分支,我们一个个来看,第一个是:
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {}
image 为空意味着缓存没有命中,SDWebImageRefreshCached
则是就算缓存命中也要下载图片更新缓存,SDWebImageManager
这个类还定义了一个协议并实现一个代理。
@protocol SDWebImageManagerDelegate <NSObject>
@optional
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
@end
第一个方法的意思是,是否下载图片,YES 就是下载,NO 就是不下载。第二个方法是,对下载好的图片 image 做 transform 处理,比如可以改成圆角图片等,然后返回,这样缓存的图片也会是 transform 之后的图片。
理解了代理方法的意思就可以理解这个条件了,如果缓存没有命中,或需要刷新已有缓存 且 没有实现 imageManager:shouldDownloadImageForURL
的方法(默认是 YES,可以下载图片)则去下载图片。如果实现了这个代理方法返回的是 YES,也会去下载图片。
看第一个条件里的代码,首先:
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
completedBlock(image, nil, cacheType, YES, url);
});
}
缓存如果命中,且需要更新缓存,则先将缓存图片通过 completedBlock 回调出去,在继续下载图片。
在往下就是缓存没有命中或需要更新缓存的情况,所以需要下载图片,但之前先将 SDWebImageManager
里 option 的条件映射成 SDWebImageDownloder
里的 option 的条件,下载使用的方法是 SDWebImageDownloder
里的 - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
方法。具体实现我们在分析 SDWebImageDownloder
时会说,先看 completedBlock 里的逻辑。
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
// Do nothing if the operation was cancelled
// See #699 for more details
// if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
}
这里注意避免循环引用,imageDownloader
被 SDWebImageManager
强引用,downloadImageWithURL
的 completedBlock 会被 imageDownloader
的属性 URLCallbacks
数组强引用保存起来,至于为什么这么做后面会讲到。
然后是发生错误的处理情况:
else if (error) {
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
发生错误,并回调。将在确定条件下失败的 URL 放入黑名单,不会反复请求。(通常是 URL 的问题,而不是网络问题)
看下 else 之后的代码,稍长一些:
else {
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
}
else 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);
}
});
});
}
else {
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);
}
});
}
}
如果 option 是 SDWebImageRetryFailed
则这个 url 从黑名单中删除,给它一个重试的机会。有 downloadedImage
说明图片下载成功,如果是要进行 transform ,则调用 delegate 方法,获取 transform 之后的图片,进行缓存,再调用 completedBlock 。如果不需要 transform 则直接缓存后回调 completedBlock。
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
};
这是下载图片的取消操作,调用 NSOperation 的 cancel,从 self.runningOperations
中删除 operation。赋值给 cancelBlock ,交给 SDWebImageCombinedOperation
对象管理。
看前面提到的三个分支条件的第二个:
else 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];
}
}
有 image 说明缓存命中,且没有要重下图片的情况,则直接回调 completedBlock 就可以了。
第三个分支条件,它的意思是既没有缓存图片,代理 delegate 也不允许下载图片,那就只能直接回调 completedBlock ,图片参数传 nil 了。
else {
// Image not in cache and download disallowed by delegate
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
到此,SDWebImageManager
的核心方法:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
就介绍完了。
图片下载器 SDWebImageDownloader
我们还是从 SDWebImageDownloader
的核心方法入手:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
该方法的大部分代码都放到了一个 createCallback 的回调中:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
...
}];
return operation;
}
那我们只能先去探究下 - (void)addProgressCallback: completedBlock: forURL: 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 参数不能为空,因为它要作为存储 callbacks 字典的 key,如果它为 nil 则会马上调用 completed block 返回 nil 图片和 nil 数据。
self.URLCallbacks
是一个 NSMutableDictionary
,它以 URL 作为 key ,维护一个可变数组 callbacksForURL
,这个数组里又会存放一个一个的 NSMutableDictionary
用来存储两个 callback 回调方法,分别是以 kProgressCallbackKey
为 key 的 progressBlock
和 以 kCompletedCallbackKey
为 key 的 completedBlock
。代码里还有一个 BOOL first
的变量,如果发现 self.URLCallbacks
中没有这个 URL 的回调数组,那这个 URL 此时就是第一次请求(此时没有相同的 URL 在请求),会调用 createCallback();
来创建下载的操作,而发现 self.URLCallbacks
中有这个 URL 的回调数组的话,则将对应的那两个回调方法存进 NSMutableDictionary
,在放到之前的回调数组中,且不会再调用 createCallback();
,这样相同的 URL 不会重复请求下载。当第一个请求下载成功之后,会遍历这个回调数组,将数组里所有的 callback 都执行一遍。这么做的目的就是防止同时有多个相同 URL 的请求发生。
这段代码使用 dispatch_barrier_sync
将任务放入一个并发队列,目的是在并发队列中,这个任务执行时,不允许别的任务同时执行。因为 downloadImageWithURL:
方法要返回一个遵从<SDWebImageOperation>
的对象,所以要同步执行而不能异步。
现在就可以看一看 createCallback();
中到底都做了什么。
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
operation = [[wself.operationClass alloc] initWithRequest:request
inSession:self.session
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
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);
}
}
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
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;
}
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
先创建了 NSMutableURLRequest
请求对象,然后调用 SDWebImageDownloaderOperation
(继承 NSOperation)的方法:
- (id)initWithRequest:(NSURLRequest *)request
inSession:(NSURLSession *)session
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock;
在 progressBlock 的回调方法里,会通过 sself.URLCallbacks
取出这个 URL 所有 kProgressCallbackKey
的回调方法,并将获取到的 receivedSize
和 expectedSize
的值传入这些方法中调用。
在 completedBlock 的回调方法里和 progressBlock 中的一样,取出 kCompletedCallbackKey
对应的回调方法,将获取到的 image ,data,error,finished 的值传入方法中调用,还会删除 sself.URLCallbacks
中这个 URL 的回调数组 ,保障这个 URL 下次可以重新创建新请求。
在 cancelBlock 中则只是删除掉 sself.URLCallbacks
中这个 URL 的回调数组。
在往下是,给 operation 设置是否应该解压图片的属性,解压图片会提高下载和缓存的性能,但是会消耗较多的内存,如果程序因为占用内存过多而闪退则要把这个属性设置成 NO。
设置 operation 请求的 NSURLCredential
,用于在请求过程中,服务端要求验证客户端的凭证 - (void)URLSession: task: didReceiveChallenge: completionHandler:
。
再往后,是设置 NSOperation 的操作优先级。[wself.downloadQueue addOperation:operation];
是将操作任务加到 NSOperationQueue
队列中,开始任务。最后是设置操作的执行顺序,默认是 FIFO 的先进先出的模式,也可以改成 LIFO 后进先出的栈模式,实现的方法就是添加依赖,前面的操作依赖后面的操作。设置完之后,则 return 这个 operation。
到此,SDWebImageDownloader
的这个核心方法就介绍完了。
还有一点,下载的请求是使用的 NSURLSession
,SDWebImageDownloader
将 NSURLSession
的 delegate 设置成自己,统一接收这些回调方法。在这些回调方法中,会返回一个 NSURLSessionDataTask
通过这个 dataTask 的 taskIdentifier
,我们就可以在 self.downloadQueue.operations
中找到回调方法对应的 operation (SDWebImageDownloaderOperation
),每个 operation 中都有这些代理方法,这样在 SDWebImageDownloader
统一接收的回调中用找到的 operation 调用当前的这个代理方法,把参数传到对应的 operation 中。
代码如下:
- (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
SDWebImageDownloaderOperation *returnOperation = nil;
for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
returnOperation = operation;
break;
}
}
return returnOperation;
}
#pragma mark NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
[dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
...
SDWebImageDownloaderOperation
的 - (id)initWithRequest:(NSURLRequest *)request inSession: options: progress: completed: cancelled:;
是下载图片的关键代码,下面就来看下 SDWebImageDownloaderOperation
这个类。
操作单元 SDWebImageDownloaderOperation
SDWebImageDownloaderOperation
继承自 NSOperation
,并且实现了 <SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
这三个协议。继承 NSOperation
的子类执行任务的代码都写在 - (void)start;
或者 - (void)main;
中,我们就从 SDWebImageDownloaderOperation
重写的 - (void)start;
方法入手。
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
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:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
这段代码的意思是,如果程序进入后台会给程序一段时间,完成未完成的任务,如果时间到了任务还是没有完成则取消这个任务。
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
self.thread = [NSThread currentThread];
self.unownedSession
是从 SDWebImageDownloader
中传进来的,而如果没有传进来 self.unownedSession
则自己创建一个 self.ownedSession
,这个 self.ownedSession
设置的代理是自己,回调的代理方法直接调用这个类里的, 而self.unownedSession
传进来的这种,代理方法就是通过上面介绍过的方式调用到这个类的。上面注释的意思是为 task 创建一个 session,delegateQueue 中传入一个 nil,这样 session 就会创建一个串行的操作队列来执行所有的代理方法和完成处理的调用。
继续看代码:
[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 {
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
resume 开启这个任务,调用下 self.progressBlock
传入初始的值,然后在主线程发送一个开始下载的通知,如果没有 self.dataTask
则调用 self.completedBlock
返回一个 NSError
。
下面我们在简单说下 SDWebImageDownloaderOperation
类中这两个 NSURLSessionTaskDelegate
和 NSURLSessionDataDelegate
协议的代理方法。
#pragma mark NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
在这个方法里检查下 response 的状态码,不正确的话取消任务,completedBlock 回调 NSError
。正确的话,获取下载数据的总大小 expectedContentLength
,并调用 self.progressBlock
。还会创建保存数据流的 NSMutableData
对象,self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
将每次接收到的数据 data 拼接到之前创建好的 self.imageData
中去,[self.imageData appendData:data];
。如果 option 的要求是 SDWebImageDownloaderProgressiveDownload
则在这里把已有的数据 self.imageData
转成 image ,通过 self.completedBlock 回调出去,注意 finished 参数是 NO。
if (self.progressBlock) {
self.progressBlock(self.imageData.length, self.expectedSize);
}
调用 self.progressBlock
将下载进度回调出去。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
请求完成的回调方法,有 error 则 self.completedBlock(nil, nil, error, YES);
,没有则将 self.imageData
转成 image 回调出去 completionBlock(image, self.imageData, nil, YES);
,当然这里面涉及很多处理的细节和其他情况的判断,就先不说了。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;
这段则是针对不同的鉴定场景返回不一样的策略。
至此,SDWebImageDownloaderOperation
中的这些代理方法就简单的介绍完了。
图片缓存 SDImageCache
SDImageCache
包括内存缓存和磁盘缓存,内存缓存使用的是继承自 NSCache
的 AutoPurgeCache
,而磁盘缓存就是基于文件的读写。
先查看 SDImageCache
的接口,看下都包括哪些功能,然后一一讲解代码。
存储的功能:
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key;
这四个方法的前两个直接调用的第三个,所以我们从第三个方法入手。
看代码:
// if memory cache is enabled
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
如果内存缓存可用,就将图片通过 NSCache
的接口 - (void)setObject: forKey: cost: ;
存入。计算 cost 的方法是:
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
return image.size.height * image.size.width * image.scale * image.scale;
}
也就是一张图片的像素数量。
如果需要存入磁盘,一般情况下我们是将 imageData 直接存入的,但是如果 recalculate
的值是 YES ,或者没有 imageData,那我们就需要将 image 转成 NSData
存入磁盘。具体的实现是判断这个 image 有没有透明通道或者它的前八个字节是不是规定的 PNG 那固定的八个字节,如果是则就调用 UIImagePNGRepresentation
方法转成 NSData
,如果不是那就调用 UIImageJPEGRepresentation
这个方法。有了 data 之后,就要调用那四个存储方法的第四个 storeImageDataToDisk
。
通过 key 和 _diskCachePath
得到缓存文件的具体路径,在使用 NSFileManager
中 - (BOOL)createFileAtPath: contents: attributes: ;
方法,将数据写入磁盘中。
// disable iCloud backup
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
这段代码是避免该文件被 iCloud 备份。
这些读写操作都放到了 SDImageCache
的一个串行队列中,ioQueue
。我觉得是因为 _fileManager
是自己创建的:
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
是为了保障它的线程安全,在 SDImageCache
这个类的所有文件读写操作,都会放到 ioQueue
这个队列执行。而 [NSFileManager defaultManager]
是系统提供,本身就是线程安全的。
查询的功能:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
第一个查询方法,在讲 SDWebImageManager
时已经讲过了。
第二个方法,就是调用的 NSCache
中的 - (nullable ObjectType)objectForKey:
的方法。
第三个方法中,会先到内存缓存去查找,如果没有命中,则去磁盘缓存中查找,大概就是通过 key 获取具体的路径找到对应的文件取出 NSData
,在经过一些处理转成 image 返回。
删除的功能:
- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
前三个方法都是调用的第四个,所以我们看第四个方法就好了。
如果有内存缓存则调用 NSCache
中的 - (void)removeObjectForKey:
,如果 fromDisk
为 YES,则调用 NSFileManager
的 - (BOOL)removeItemAtPath: error:
方法,删除指定缓存文件的路径即可。
清除的功能:
- (void)clearMemory;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;
第一个方法直接调用 NSCache
的 - (void)removeAllObjects;
。第二个方法,直接调用了 NSFileManager
的 - (BOOL)removeItemAtPath: error:
删除指定缓存目录的路径即可。第三个方法调用的第二个方法。
清理的功能:
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;
清理缓存就是清理掉一些过期的文件和超最大缓存大小限制的文件。
看第一个方法,首先获取磁盘缓存的路径 URL。然后通过以下代码获取所有缓存文件的一些属性:
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
这些属性分别是,是否是目录,文件的修改日期和文件大小。
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
这一句则是获取缓存过期的日期。
然后 for-in 遍历 fileEnumerator
:
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// Skip directories.
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
获取文件路径的属性字典,如果是目录则跳过,比较修改日期和过期日期哪个更晚一些,如果是过期日期则说明该文件过期,放入 urlsToDelete
数组中。将文件大小累加到 currentCacheSize
上,并将不是过期的这些缓存文件记录到 cacheFiles
中,key 是文件的 URL ,value 是对应的属性字典。
之后,遍历 urlsToDelete
数组删除这些过期文件:
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
然后,判断没有过期的这些文件的总大小有没有超过最大的缓存大小 self.maxCacheSize
。
如果有的话,将 cacheFiles
里的 value 按照文件的修改日期进行排序,返回一个排好序的数组。取 self.maxCacheSize
大小的一半,作为清理缓存的界限 const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
。遍历排序后的数组,一个个文件删除,删除一个就从之前的总缓存文件大小的值减去删除后的文件大小,再比较有没有小于清理缓存的界限值 desiredCacheSize
。如果小于了,则跳出循环。最后在主线程回调 completionBlock();
。这样就达到了清理磁盘缓存的目的。
计算缓存大小:
- (NSUInteger)getSize;
- (NSUInteger)getDiskCount;
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
第一个方法就是遍历缓存目录的所有文件,获取这些文件路径,通过 [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil]
获得一个字典在通过 fileSize 方法获取文件大小,累加起来就是缓存的大小。
第二个和第三个方法都是获取指定缓存路径的 NSDirectoryEnumerator
遍历取对应的值,和上面相差不大,不在赘述。
查询缓存是否存在:
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;
这些方法的实现基本就是调用 exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]];
这个方法,不在赘述。
最后说下 clearMemory
,cleanDisk
,backgroundCleanDisk
的调用时机,在 - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory{}
这个初始化方法中,注册了三个通知分别是:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
报内存警告时调用 clearMemory
清除内存缓存,程序即将终止的时候调用 cleanDisk
清理过期或超大小限制的磁盘缓存,而程序进入后台的时候,调用 backgroundCleanDisk
,在后台执行 cleanDiskWithCompletionBlock
清理任务。
至此,SDImageCache
的大部分方法就讲解完了。
结束
SDWebImage
这个库的基本思路就说完了,其实里面还有诸多的细节需要学习,比如图片处理,性能优化,内存管理等。阅读优秀的开源代码,会有一种探索的乐趣,不论从大的整体结构还是小的实现细节上,都能学到很多东西。
网友评论
我这里有个疑惑,UIImageView+WebCache.m中主要方法的操作
1.从字典中移除当前图片的加载 - main thread
2.初始化operation图片加载操作 (内部是在非main thread)
3.将该operation扔到字典中 - main thread
我想知道的是当我连续执行2次- (void)sd_setImageWithURL:的时候,他是会操作完这3个步骤再开始第二条- (void)sd_setImageWithURL:的执行吗
然后,注意sd_setImageWithURL方法的第一句: [self sd_cancelCurrentImageLoad]; 就是根据key从字典中取出当前正在下载图片的opetation,并取消下载,所以下载可能会被取消,然后重新下载 ~( ̄(oo) ̄)ノ