起
從sd_setImageWithURL:方法談SDWebImage (一)
從sd_setImageWithURL:方法談SDWebImage (二)
正
上篇文章從sd_setImageWithURL:
方法談到SDWebImageManager。
而SDWebImageManager在SDWebImage中的身份是屬于協(xié)調(diào)管理的角色而非執(zhí)行者。SDWebImageManager主要是協(xié)調(diào)SDImageCache和SDWebImageDownloader的單例對(duì)象。
SDImageCache *imageCache
SDWebImageDownloader *imageDownloader
直接從GitHud上拉到作者提供的圖。很直觀的看到了SDWebImage下載圖片的流程:
- sd_setimageWithURL(),從最初調(diào)用設(shè)置圖片方法。
- sd_internalSetImageWithURL(),所有設(shè)置圖片的UIKit的分類最終會(huì)調(diào)用UIView+WebCache中的sd_internalSetImageWithURL()來下載圖片。
- SDWebImageManager.sharedManager開啟加載
loadImageWithURL()方法。 - loadImageWithURL()中首先會(huì)調(diào)用SDImageCache對(duì)象的queryCacheOperationForKey()方法查找緩存,首先查找內(nèi)存中是否有圖片的緩存,如果沒有繼續(xù)查找磁盤緩存。
- 如果查找命中,返回image對(duì)象、和imageData。
- 內(nèi)存和磁盤中都沒有找到圖片。使用SDWebImageDownloader對(duì)象開啟圖片下載任務(wù)。
- 返回SDWebImageDownloader對(duì)象圖片下載的結(jié)果到SDWebImageManager。
- SDWebImageManager再調(diào)用SDImageCache對(duì)象將圖片緩存值到內(nèi)存和磁盤中(實(shí)際情況根據(jù)SDWebImageOptions的設(shè)置)。
- 將圖片返回到UIView+WebCache中,調(diào)用completionBlock回調(diào)。
- 最后根據(jù)UI的控件根據(jù)自身的情況設(shè)置圖片。在WebCache Categories文件夾中的分類,UIView+WebCache除了是用來下載,還使用默認(rèn)或自定義block來設(shè)置圖片到UI控件中。
上面的操作過程是SDWebImage主要的功能流程。其中1、2、3、9、10的步驟在前一篇的文章中從sd_setImageWithURL:方法談SDWebImage 起有過介紹。
剩下的將會(huì)在本文中介紹。
SDWebImageManager中的核心方法如下,在下面的代碼中,我添加了一些注釋去除了一些無關(guān)緊要的代碼,使用文字描述帶過。不會(huì)對(duì)源碼的理解造成影響。
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
// 如果url錯(cuò)誤、或者下載options沒有重試的選項(xiàng)且已經(jīng)下載失敗過一次的url 直接返回初始的operation拋出錯(cuò)誤。
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
// 磁盤緩存沒有命中 || 刷新磁盤緩存 || 針對(duì)當(dāng)前的url是否需要下載(默認(rèn)YES,開發(fā)者可以根據(jù)代理配置為NO)開啟下載任務(wù)
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
if (cachedImage && options & SDWebImageRefreshCached) {
// 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.
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// 設(shè)置下載任務(wù)的options:如優(yōu)先級(jí)、下載進(jìn)度條是否顯示等,參考SDWebImageOptions
// 開啟下載任務(wù),返回下載任務(wù)的token用于取消操作
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
// 任務(wù)取消,不做處理
} else if (error) {
// 下載失敗,調(diào)用完成回調(diào)
[self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
// 合理的錯(cuò)誤將url加入失敗的url數(shù)組。便于重試下載(合理是指:url對(duì)應(yīng)的真實(shí)資源、非用戶主動(dòng)取消下載等)
if (error.code) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else {
// 從失敗數(shù)組中移除
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && cachedImage && !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];
// 對(duì)圖片緩存(比對(duì)處理后的圖片是否有所改動(dòng)。改動(dòng)后不一致會(huì)重新對(duì)圖片encodeData)
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
// 回調(diào)
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
if (downloadedImage && finished) {
// 直接對(duì)下載完成的圖片緩存、
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
// 回調(diào)
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
// 從下載數(shù)組中移除當(dāng)前完成的下載operation
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
operation.cancelBlock = ^{
// 取消下載、移除下載中的記錄
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
} else if (cachedImage) {
// 緩沖命中 回調(diào)返回 移除下載記錄
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
// 緩沖未命中、不允許下載直接回調(diào)返回 移除下載記錄
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}];
return operation;
我們可以看到loadImageWithURL:方法中:
- 首先根據(jù)url獲取對(duì)應(yīng)圖片緩存的key值
- self.imageCache開始查詢緩存
- 緩存命中根據(jù)options判斷是否需要刷新緩存或者緩存未命中,開啟下載任務(wù)。
-
self.failedURLs
進(jìn)行操作時(shí)都加上synchronized同步鎖,防止多線程問題。
SDWebImage根據(jù)緩存是否命中決定是否下載圖片(除了開發(fā)時(shí)指定需要刷新本地緩存)。
圖片緩存
self.imageCache
是SDWebImageManager管理的一個(gè)緩存單例。self.imageCache
使用url作為查詢的key值,在內(nèi)存和磁盤上開始查詢。首先會(huì)查詢內(nèi)存中是否存在圖片。存在返回圖片數(shù)據(jù),不會(huì)再往下查詢。不存在則再進(jìn)行磁盤查詢,查詢磁盤緩存時(shí)以u(píng)rl為key經(jīng)過MD5計(jì)算后拼接得到的完整磁盤路徑后異步訪問圖片(讀取磁盤內(nèi)容)。緩沖如果命中后會(huì)首先進(jìn)行內(nèi)存緩沖,便于下次使用。最后都會(huì)調(diào)用done:
的回調(diào)返回查找結(jié)果。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
// 讀取內(nèi)存(NSCache)中的圖片
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
if (image.images) {
// 如果是動(dòng)圖,讀取圖片data
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
// 在內(nèi)存中查詢不到圖片數(shù)據(jù)
NSOperation *operation = [NSOperation new];
// 耗時(shí)的io操作(磁盤查詢)異步在ioQueue隊(duì)列中
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
// 判斷讀取操作是否被取消保護(hù)
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = [self diskImageForKey:key];
// 保存圖片到內(nèi)存中
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
// 回調(diào)
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
// 異步操作時(shí)返回operation,便于取消磁盤查找
return operation;
}
圖片下載
如果查詢緩存未命中,SDWebImageManager就會(huì)使用self.imageDownloader
進(jìn)行網(wǎng)絡(luò)圖片的下載。即調(diào)用的downloadImageWithURL:options:progress:completed:
。在downloadImageWithURL:options:progress:completed:
內(nèi)部直接調(diào)用到了SDWebImageDownloader的addProgressCallback:completedBlock:forURL:createCallback:
方法.主要的作用是在createCallback中會(huì)創(chuàng)建一個(gè)NSOperation的自定義子類SDWebImageDownloaderOperation
以u(píng)rl為key存放到self.URLOperations
字典中,同時(shí)對(duì)operation綁定其對(duì)應(yīng)的下載進(jìn)度回調(diào)(progressBlock)和完成下載的回調(diào)(completedBlock)并自動(dòng)加入到下載隊(duì)列(self.downloadQueue
)中開啟下載任務(wù)(self.downloadQueue:
默認(rèn)最大的maxConcurrentDownloads
為6)。
每個(gè)SDWebImageDownloaderOperation的對(duì)象遵守了NSURLSession的各個(gè)代理方法。所以在下載過程中,在NSURLSession的代理方法上調(diào)用對(duì)應(yīng)的progressBlock下載進(jìn)度和最后下載完成時(shí)調(diào)用completedBlock。
createCallback:部分主要代碼
SDWebImageDownloaderOperation(^createCallback)(void) =^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
// 設(shè)置下載圖片的時(shí)間(默認(rèn)15.0)
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 創(chuàng)建一個(gè)網(wǎng)絡(luò)請(qǐng)求request,設(shè)置一系列屬性:HTTPHeaders、cookie、緩存策略、是否等待相應(yīng)返回等
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
// operation內(nèi)部遵守了NSURLSession的代理方法
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
// 是否解壓圖片
operation.shouldDecompressImages = sself.shouldDecompressImages;
// 設(shè)置url請(qǐng)求的驗(yàn)證
// 設(shè)置下載的優(yōu)先級(jí)
// 加入隊(duì)列開始下載任務(wù)
[sself.downloadQueue addOperation:operation];
// 設(shè)置任務(wù)之間的依賴
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
注意operation.shouldDecompressImages
設(shè)置為YES時(shí),網(wǎng)絡(luò)上有人碰到下載高清大圖會(huì)有內(nèi)存的問題。關(guān)于圖片解壓縮的問題可以看看這篇文章談?wù)?iOS 中圖片的解壓縮
當(dāng)然對(duì)每個(gè)未完成下載operation多次設(shè)置下載時(shí),都會(huì)先用ur為用key對(duì)字典self.URLOperations
進(jìn)行取值。為nil才會(huì)調(diào)用createCallback();如果存在也只是重新綁定一次progressBlock
和completedBlock
。
注意downloadImageWithURL:options:progress:completed:
會(huì)返回一個(gè)SDWebImageDownloadToken
的對(duì)象用來取消圖片下載操作。SDWebImageDownloadToken
組合了url
和progressBlock
、completedBlock
回調(diào)。在需要取消的時(shí)候使用url
從self.URLOperations
取出對(duì)應(yīng)的下載operation進(jìn)行cancel
、和取消對(duì)應(yīng)的回調(diào)。
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// 返回后續(xù)用來取消下載操作的組合token
__block SDWebImageDownloadToken *token = nil;
dispatch_barrier_sync(self.barrierQueue, ^{
// 讀取是否正在下載
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
// 創(chuàng)建新的下載任務(wù)加入到URLOperations中
operation = createCallback();
self.URLOperations[url] = operation;
// 設(shè)置完成回調(diào),從URLOperations移除當(dāng)前operation
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
});
};
}
// 將operation的回調(diào)progressBlock、completedBlock組合callbackBlocks。便于取消
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
// token組合url和downloadOperationCancelToken。
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}
注意 源碼中多處出現(xiàn)dispatch_barrier_async
、dispatch_barrier_sync
的配套使用,主要是確保線程安全。
結(jié)
以上介紹了 SDWebImage主要的下載流程。但SDWebImage在圖片下載的過程為我們過濾掉了很多的不利情況并且做了很多的優(yōu)化和代碼實(shí)現(xiàn)。這些都是如果我們不深入代碼是不會(huì)了解到。比如磁盤緩存圖片路徑對(duì)key的MD5處理防止重名、圖片的編碼轉(zhuǎn)換、圖片的解壓縮、圖片緩存(NSCache)的清理、自定義NSOperation。
在下一篇中會(huì)介紹下SDWebImage中比較重要的技術(shù)點(diǎn)。