顧名思義,SDWebImageDownloader
是SDWebImage
庫中負(fù)責(zé)圖片下載的管理工作凫岖。它維護(hù)著下載圖片用的NSURLSession
江咳,但不直接與網(wǎng)絡(luò)打交道。它管理著一系列的SDWebImageDownloaderOperation
哥放,下載的實(shí)際工作都由SDWebImageDownloaderOperation
進(jìn)行歼指。
這兩個(gè)類合起來成為了一個(gè)異步并發(fā)機(jī)制的設(shè)計(jì)范本,可以作為后續(xù)開發(fā)類似機(jī)制的參考甥雕。
以下代碼和分析都基于041842bf085cbb711f0d9e099e6acbf6fd533b0c這個(gè)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
類支持從外部設(shè)置session(多個(gè)Opeations共享一個(gè)session)和自己維護(hù)一個(gè)session兩種模式。
為了行為統(tǒng)一社露,SDWebImageDownloader
類實(shí)現(xiàn)了NSURLSessionTaskDelegate
和NSURLSessionTaskDelegate
這兩個(gè)協(xié)議挟阻,但是把這些協(xié)議的具體實(shí)現(xiàn)都轉(zhuǎn)交給了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
為了保證同一個(gè)URL只發(fā)出一個(gè)網(wǎng)絡(luò)請(qǐng)求峭弟,SDWebImageDownloader
維護(hù)了一個(gè)以URL為key的字典self.URLOperations
保存所有的SDWebImageDownloaderOperation
附鸽。
這個(gè)方法可能會(huì)在任何線程被調(diào)用,為了防止出現(xiàn)多線程同時(shí)操作self.URLOperations
的問題瞒瘸,SDWebImageDownloader
維護(hù)了一個(gè)專門的隊(duì)列處理對(duì)self.URLOperations
的操作坷备。
仔細(xì)看這里的代碼會(huì)發(fā)現(xiàn)一個(gè)問題,NSOperation的completionBlock
也對(duì)self.URLOperations
進(jìn)行了操作情臭,但是它沒有被放在self.barrierQueue
里執(zhí)行省撑。已經(jīng)有網(wǎng)友針對(duì)這個(gè)問題給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
上面提到,多個(gè)地方可能同時(shí)觸發(fā)同一個(gè)URL的圖片下載竟秫,'SDWebImageDownloader'重用了下載用的SDWebImageDownloaderOperation
對(duì)象惯雳。但是這一優(yōu)化對(duì)于外部是透明的,SDWebImageDownloaderOperation
要保證每一個(gè)調(diào)用下載的對(duì)象都能正確接收到下載完成的回調(diào)鸿摇。
SDWebImageDownloaderOperation
保存了callbackBlocks
這個(gè)數(shù)組用于維護(hù)所有的回調(diào)block。下載進(jìn)度更新和下載完成的回調(diào)block被合起來以SDCallbacksDictionary
的形式保存在callbackBlocks
里劈猿。同時(shí)拙吉,這個(gè)SDCallbacksDictionary
的指針還被返回作為取消請(qǐng)求用的標(biāo)記。
和SDWebImageDownloader
一樣揪荣,SDWebImageDownloaderOperation
也維護(hù)了一個(gè)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);
}
});
}
不管圖片下載完成還是出現(xiàn)異常,completionBlock
都會(huì)被拋到主線程處理仗颈。
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
可以通過給要測(cè)試的類加一個(gè)Category的方式來暴露其私有屬性和方法給測(cè)試佛舱,不需要為了測(cè)試把私有方法提到接口中。
SDWebImage源碼系列
SDWebImage源碼之SDImageCache
SDWebImage源碼之SDWebImageDownloader
SDWebImage源碼之SDWebImageManager