顾名思义,SDWebImageDownloader
是SDWebImage
库中负责图片下载的管理工作。它维护着下载图片用的NSURLSession
,但不直接与网络打交道。它管理着一系列的SDWebImageDownloaderOperation
,下载的实际工作都由SDWebImageDownloaderOperation
进行。
这两个类合起来成为了一个异步并发机制的设计范本,可以作为后续开发类似机制的参考。
以下代码和分析都基于041842bf085cbb711f0d9e099e6acbf6fd533b0c这个commit。
SDWebImageDownloader & SDWebImageDownloaderOperation
@interface SDWebImageDownloader () <NSURLSessionTaskDelegate, 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];
}
@end
---
@interface SDWebImageDownloaderOperation : NSOperation <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;
- (void)start {
...
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.dataTask resume];
...
}
@end
SDWebImageDownloaderOperation
类支持从外部设置session(多个Opeations共享一个session)和自己维护一个session两种模式。
为了行为统一,SDWebImageDownloader
类实现了NSURLSessionTaskDelegate
和NSURLSessionTaskDelegate
这两个协议,但是把这些协议的具体实现都转交给了SDWebImageDownloaderOperation
。
@interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
...
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
operation = createCallback();
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
};
}
});
...
}
- (void)cancel:(nullable SDWebImageDownloadToken *)token {
dispatch_barrier_async(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
if (canceled) {
[self.URLOperations removeObjectForKey:token.url];
}
});
}
@end
为了保证同一个URL只发出一个网络请求,SDWebImageDownloader
维护了一个以URL为key的字典self.URLOperations
保存所有的SDWebImageDownloaderOperation
。
这个方法可能会在任何线程被调用,为了防止出现多线程同时操作self.URLOperations
的问题,SDWebImageDownloader
维护了一个专门的队列处理对self.URLOperations
的操作。
仔细看这里的代码会发现一个问题,NSOperation的completionBlock
也对self.URLOperations
进行了操作,但是它没有被放在self.barrierQueue
里执行。已经有网友针对这个问题给SDWebImage
提了Pull Request。
@interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
...
__block SDWebImageDownloadToken *token = nil;
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
@end
---
typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
@interface SDWebImageDownloaderOperation ()
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks addObject:callbacks];
});
return callbacks;
}
- (BOOL)cancel:(nullable id)token {
__block BOOL shouldCancel = NO;
dispatch_barrier_sync(self.barrierQueue, ^{
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
});
if (shouldCancel) {
[self cancel];
}
return shouldCancel;
}
@end
上面提到,多个地方可能同时触发同一个URL的图片下载,'SDWebImageDownloader'重用了下载用的SDWebImageDownloaderOperation
对象。但是这一优化对于外部是透明的,SDWebImageDownloaderOperation
要保证每一个调用下载的对象都能正确接收到下载完成的回调。
SDWebImageDownloaderOperation
保存了callbackBlocks
这个数组用于维护所有的回调block。下载进度更新和下载完成的回调block被合起来以SDCallbacksDictionary
的形式保存在callbackBlocks
里。同时,这个SDCallbacksDictionary
的指针还被返回作为取消请求用的标记。
和SDWebImageDownloader
一样,SDWebImageDownloaderOperation
也维护了一个self.barrierQueue
用作操作self.callbackBlocks
。
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
error:(nullable NSError *)error
finished:(BOOL)finished {
NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
dispatch_main_async_safe(^{
for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
completedBlock(image, imageData, error, finished);
}
});
}
不管图片下载完成还是出现异常,completionBlock
都会被抛到主线程处理。
SDWebImageDownloaderTests
/**
* Category for SDWebImageDownloader so we can access the operationClass
*/
@interface SDWebImageDownloader ()
@property (assign, nonatomic, nullable) Class operationClass;
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback;
@end
可以通过给要测试的类加一个Category的方式来暴露其私有属性和方法给测试,不需要为了测试把私有方法提到接口中。
SDWebImage源码系列
SDWebImage源码之SDImageCache
SDWebImage源码之SDWebImageDownloader
SDWebImage源码之SDWebImageManager