圖片下載中的歸并下載
如圖所示,其實(shí)就是把并發(fā)中的相同請(qǐng)求連接,回調(diào)進(jìn)行綁定,真正的網(wǎng)絡(luò)請(qǐng)求只維持一份,請(qǐng)求結(jié)束后再統(tǒng)一回調(diào)

1.png
SDWebImage 比較早就實(shí)現(xiàn)了該方案
由于 SD 最新的 4.x 跟 3.x api 上不兼容,導(dǎo)致我們替換起來很麻煩,而且核心功能并沒有變化,所以我們還是使用 3.x 當(dāng)中最新的 3.8.2 版本。
下面我們 走入 SDWebImage 的源碼,了解下 SD 中的歸并下載是如何實(shí)現(xiàn)的。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 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];
}
... 忽略一堆的代碼
NSString *key = [self cacheKeyForURL:url];
// 其實(shí)在讀取磁盤緩存這步就可以做 歸并 處理了,但是 disk io 損耗并不大,SD沒做,而且也不是我們的重點(diǎn)
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
...
繼續(xù)忽略一堆的代碼
有緩存就直接返回圖片緩存,無緩存就準(zhǔn)備開始下載,我們來看重點(diǎn)的下載代碼
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
其實(shí)是在 SDImageDownloadManager 去創(chuàng)建 operation
- (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:^{
// 創(chuàng)建請(qǐng)求的代碼
}];
繼續(xù)跟
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)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, ^{
// 判斷是否有該 URL 的請(qǐng)求對(duì)象, 只有 第一個(gè)進(jìn)來的下載請(qǐng)求,才會(huì)創(chuàng)建
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];
// 把相應(yīng)回調(diào)存在 array 中
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback();
}
});
}
其實(shí)整體代碼并沒有什么問題 , 再來看創(chuàng)建 operation 的代碼
// 當(dāng)命中歸并下載邏輯后,后續(xù)返回給外部的 operation 都是 nil,并不會(huì)走到 createCallback 內(nèi)部
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
... 一堆 request 初始化代碼
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:^{
// 當(dāng)?shù)谝粋€(gè) operation 被cancel 掉時(shí),其他回調(diào)就統(tǒng)一沒了【加紅加粗】
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
operation.shouldDecompressImages = wself.shouldDecompressImages;
發(fā)現(xiàn)的問題
- 當(dāng)命中歸并下載邏輯后,后續(xù)返回給外部的 operation 都是 nil
- 當(dāng)?shù)谝粋€(gè)operation執(zhí)行cancel后,后續(xù)的 operation 都被取消了, 相當(dāng)于同時(shí)兩個(gè)View都在下載,第一個(gè)取消了,導(dǎo)致第二個(gè)也顯示不出來
- 當(dāng)后續(xù)的 imageView 下載新圖片時(shí),舊的 operation 的回調(diào)并不會(huì)清除,有概率出現(xiàn)圖片顯示錯(cuò)亂的問題 (官方3.x后續(xù)版本已修復(fù))
原因如圖: cancel old operation 根本就沒用的

2.png
// 邏輯跟我們后續(xù)做的類似,也是用個(gè) operation 對(duì)真實(shí)的 downloadOperation 進(jìn)行包裝
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
... 在回調(diào)的 complatedBlock 中判斷,封裝的operation 是否被調(diào)用了取消,如果已經(jīng)取消就不操作了
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
__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
}
解決方案:應(yīng)該全部 operation 都取消了,才能取消下載
那要怎么修改呢?
- 首先要把 operation 的返回方式改掉,因?yàn)榉祷?nil,外部執(zhí)行 cancel 你也感知不到。
- 當(dāng) 一個(gè) operation 被cancel 的時(shí)候,從 URLCallbacks {URL:Array[Operation]} 移除自己,當(dāng) array.count == 0 的時(shí)候,才真正調(diào)用 request cancel
- 返回假的 operation,實(shí)現(xiàn) SDWebImageOperation 的方法,保證外部邏輯無需修改
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
創(chuàng)建一個(gè) wrapOperation, 對(duì)返回的 operation 進(jìn)行替換
@interface IMYSDWebImageDownloaderOperation : NSObject <SDWebImageOperation>
@property (nonatomic, strong) SDWebImageDownloaderOperation *operation;
@property (nonatomic, copy) void (^cancelBlock)(id weakOperation);
@property (nonatomic, copy) SDWebImageDownloaderResponseBlock responseBlock;
@property (nonatomic, copy) SDWebImageDownloaderProgressBlock progressBlock;
@property (nonatomic, copy) SDWebImageDownloaderCompletedBlock completedBlock;
@end
拋棄了之前 SDWebImage 存 Map 的方法,直接改用存對(duì)象,擴(kuò)展性和性能都更強(qiáng)
- (IMYSDWebImageDownloaderOperation *)addDownloaderOperationWithParmas:(IMYWebImageDownloadParams *)params
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if ([self.downloadQueue respondsToSelector:@selector(setQualityOfService:)]) {
[self.downloadQueue setQualityOfService:NSQualityOfServiceUserInitiated];
}
});
__block IMYSDWebImageDownloaderOperation *operation = nil;
NSURL *url = params.url ?: params.request.URL;
NSString *callbacksKey = url.absoluteString;
dispatch_barrier_sync(self.barrierQueue, ^{
NSMutableArray<IMYSDWebImageDownloaderOperation *> *callbacksForURL = self.URLCallbacks[callbacksKey];
if (!callbacksForURL) {
callbacksForURL = [NSMutableArray array];
self.URLCallbacks[callbacksKey] = callbacksForURL;
}
SDWebImageDownloaderOperation *subOperation = callbacksForURL.lastObject.operation;
if (!subOperation) {
subOperation = [self createDownloaderOperationWithParmas:params callbacksKey:callbacksKey];
}
if ((params.options & SDWebImageDownloaderHighPriority) && NSOperationQueuePriorityHigh != subOperation.queuePriority) {
subOperation.queuePriority = NSOperationQueuePriorityHigh;
}
operation = [[IMYSDWebImageDownloaderOperation alloc] init];
operation.operation = subOperation;
operation.progressBlock = params.progressBlock;
operation.responseBlock = params.responseBlock;
operation.completedBlock = params.completedBlock;
[operation setCancelBlock:^(IMYSDWebImageDownloaderOperation *weakOperation) {
__block BOOL shouldCancel = NO;
id<SDWebImageOperation> downloadOperation = weakOperation.operation;
// 線程安全
dispatch_barrier_sync(self.barrierQueue, ^{
// 防止一直持有 callbacksForURL 引起的不釋放問題,其實(shí)也可以用 weak 聲明
// 當(dāng)外部 執(zhí)行 cancel 方法時(shí), 只移除自己,并且判斷是否停止 真實(shí)request
NSMutableArray *callbacksForURL = self.URLCallbacks[callbacksKey];
[callbacksForURL removeObject:weakOperation];
if (!callbacksForURL.count) {
[self.URLCallbacks removeObjectForKey:callbacksKey];
shouldCancel = YES;
}
});
if (shouldCancel) {
[downloadOperation cancel];
}
}];
[callbacksForURL addObject:operation];
});
return operation;
}

3.png
順便把整個(gè) SDWebImage 參數(shù)改為對(duì)象化,方便擴(kuò)展,
@interface IMYWebImageDownloadParams : NSObject
@property (nonatomic, strong) NSURL *url;
@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, strong) NSDictionary *header;
@property (nonatomic, assign) BOOL shouldDecompressImages;
@property (nonatomic, assign) BOOL shouldCreatesImages;
@property (nonatomic, assign) SDWebImageDownloaderOptions options;
@property (nonatomic, copy) SDWebImageDownloaderResponseBlock responseBlock;
@property (nonatomic, copy) SDWebImageDownloaderProgressBlock progressBlock;
@property (nonatomic, copy) SDWebImageDownloaderCompletedBlock completedBlock;
@end
// 原有的 SD 方法都轉(zhuǎn)為走 downloadImageWithParmas
@interface SDWebImageDownloader (IMYWebImage)
- (id<SDWebImageOperation>)downloadImageWithParmas:(IMYWebImageDownloadParams *)params;
@end
// 整體方法覆蓋沒有采用 method swizzle ,而是直接采用 category 覆蓋
#pragma mark - 覆蓋 .m 方法
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (id<SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
{
return [self downloadImageWithURL:url header:nil options:options response:nil progress:progressBlock completed:completedBlock];
}
#pragma clang diagnostic pop
@end