前言
通過前兩篇關(guān)于SDWebImage的學(xué)習(xí)著隆,我們已經(jīng)知道了它的下載策略和緩存策略。本次要學(xué)習(xí)的內(nèi)容是SDWebImage中是通過怎樣的調(diào)度方法來使用下載和緩存的美浦。
方法的調(diào)用
在使用的時候浦辨,我們通常會直接調(diào)用類別中提供的方法來加載圖片,查看它們的實現(xiàn)我們可以發(fā)現(xiàn)最終我們會調(diào)用到這樣一個函數(shù)
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary *)context;
外部提供的接口都是對這個函數(shù)的一個封裝币厕,方便我們使用時候調(diào)用芽腾。來看看具體的實現(xiàn):
里面首先對operationKey進行了檢驗,生成可用的key摊滔,并將這個key對應(yīng)的operation添加到了一個SDOperationsDictionary類的字典中店乐,并綁定到對應(yīng)的view中呻袭。后續(xù)在管理任務(wù)都會用到這個字典。
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
省略一些檢測代碼廉侧,當(dāng)URL正確時篓足,就會調(diào)用SDWebImageManager類的單利對象進行圖片下載。這個方法就會返回一個operation回來枕扫,通過[self sd_setImageLoadOperation:operation forKey:validOperationKey];
將這個operation保存到之前說到的字典中辱魁。
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
/**
省略若干代碼诗鸭,這些代碼的作用是根據(jù)用戶的設(shè)定將下載好的數(shù)據(jù)返回給上層。
*//
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
}
可以發(fā)現(xiàn)锻弓,這個函數(shù)中并沒有下載和緩存相關(guān)的代碼蝌箍,于是我們可以猜測下載和緩存相關(guān)的邏輯是放在SDWebImageManager類中進行處理的。下面我們就對調(diào)度類SDWebImageManager進行進一步的查看分析妓盲。
SDWebImageManager
查看SDWebImageManager.h文件,可以發(fā)現(xiàn)SDWebImageManager中提供了2個我們之前講到過的屬性弹沽,分別是SDWebImageDownloader
和imageCache
筋粗。這兩個實例中不就是提供下載和緩存的操作嗎?
@interface SDWebImageManager : NSObject
@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;
進一步查看丽已,還提供了一個遵循<SDWebImageManagerDelegate>
協(xié)議的代理买决。<SDWebImageManagerDelegate>
協(xié)議主要提供了2個接口:
//控制在cache中沒有找到image時 是否應(yīng)該去下載吼畏。默認(rèn)是YES瘸味。
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;
//在下載之后,緩存之前轉(zhuǎn)換圖片藕夫。在全局隊列中操作枯冈,不阻塞主線程
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;
此外,SDWebImageManager還提供了這些操作尘奏,包括圖片的下載,取消炫加,緩存,檢測等酒甸。我們需要重點學(xué)習(xí)的方法是downloadImageWithURL
下載這個函數(shù)赋铝。
- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader;
//下載圖片
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
//緩存給定URL的圖片
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
//取消當(dāng)前所有的操作
- (void)cancelAll;
//監(jiān)測當(dāng)前是否有進行中的操作
- (BOOL)isRunning;
//監(jiān)測圖片是否在緩存中,監(jiān)測結(jié)束后調(diào)用completionBlock
- (void)cachedImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//監(jiān)測圖片是否緩存在disk里,監(jiān)測結(jié)束后調(diào)用completionBlock
- (void)diskImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//返回給定URL的cache key,默認(rèn)是圖片的url
- (NSString *)cacheKeyForURL:(NSURL *)url;
對于downloadImageWithURL
下載函數(shù)來說农尖,它需要傳入4個參數(shù)良哲,和返回SDWebImageOperation類的實例。
- @param url 網(wǎng)絡(luò)圖片的 url 地址
- @param options 一些定制化選項
- @param progressBlock 下載時的 Block窟扑,其定義為:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
- @param completedBlock 下載完成時的 Block漏健,其定義為:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
- @return 返回 SDWebImageOperation 的實例
具體的流程是
1.驗證URL正確性
如果不正確,則回調(diào)error
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;
}
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
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;
}
2.檢測該URL是否被下載過殖属,去緩存中查找圖片
私有成員變量是一個NSMutableArray<SDWebImageCombinedOperation *>的數(shù)組瓦盛,用來保存所有正在進行的operation外潜,方便統(tǒng)一控制挠唆。
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
//根據(jù)url生成緩存對應(yīng)的key
NSString *key = [self cacheKeyForURL:url];
//將查找結(jié)果保存到operation中的chcheOperation中。
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
3.根據(jù)不同的情況進行不同的操作
3.1>如果在緩存中沒有找到圖片滔驾,或者采用的 SDWebImageRefreshCached 選項俄讹,則從網(wǎng)絡(luò)下載
這個操作會先調(diào)用imageDownloader
嘗試下載圖片,如果下載失敗則拋出異常摊阀,如果下載成功則會調(diào)用imageCache
將圖片進行緩存踪蹬。
//如果沒有緩存或者用戶允許刷新緩存
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
//如果在緩存中找到了圖片,直接回調(diào)跃捣。
if (cachedImage && options & SDWebImageRefreshCached) {
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
/**
省略一部分用戶定制策略的判斷
**/
//使用imageDownloader進行下載枝缔。
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
//如果這個任務(wù)被取消了蚊惯,則什么都不做
} else if (error) {
[self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
如果這個任務(wù)失敗了,則組裝error對象趴荸,拋出
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost) {
@synchronized (self.failedURLs) {
//將這個URL添加到失敗URL數(shù)組中
[self.failedURLs addObject:url];
}
}
}
else {
//下載成功宦焦,如果url曾經(jīng)失敗過,則將這個url從失敗數(shù)組中移除
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
//只是刷新緩存酝豪,則不回調(diào)
} 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), ^{
//如果下載成功精堕,并且不是GIF圖片,并且代理實現(xiàn)了圖片轉(zhuǎn)換
//則先使用代理進行圖片轉(zhuǎn)換
//將裝換后的結(jié)果進行緩存
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
// pass nil if the image was transformed, so we can recalculate the data from the image
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
//將結(jié)果回調(diào)
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
if (downloadedImage && finished) {
//緩存
[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) {
//完畢后揉阎,將operation從進行中數(shù)組中移除
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
@synchronized(operation) {
// Need same lock to ensure cancelBlock called because cancel method can be called in different queue
operation.cancelBlock = ^{
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
}
3.2>有緩存則直接返回
else if (cachedImage) {
__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];
}
3.3>沒有緩存背捌,用戶又不允許下載,則返回nil并講operation移除坑赡。
else {
// Image not in cache and download disallowed by delegate
__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];
}
其他接口
取消所有任務(wù)扭仁,從正在執(zhí)行的任務(wù)數(shù)組中獲取所有任務(wù)對象,并調(diào)用它們的cancel方法
- (void)cancelAll {
@synchronized (self.runningOperations) {
NSArray<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy];
[copiedOperations makeObjectsPerformSelector:@selector(cancel)];
[self.runningOperations removeObjectsInArray:copiedOperations];
}
}
//檢測sd當(dāng)前是否正在執(zhí)行任務(wù)
- (BOOL)isRunning {
BOOL isRunning = NO;
@synchronized (self.runningOperations) {
isRunning = (self.runningOperations.count > 0);
}
return isRunning;
}
//安全移除任務(wù)搀突,由于runningOperations可能被多個線程同時訪問熊泵,所以需要進行加鎖
- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
@synchronized (self.runningOperations) {
if (operation) {
[self.runningOperations removeObject:operation];
}
}
}
小結(jié):
- 調(diào)度層下載的主要流程是什么
查找緩存,若緩存中沒有 image 則通過 SDWebImageDownloader 來進行下載顽分,下載完成后通過 SDImageCache 進行緩存,會同時緩存到 memCache 和 diskCache 中
- 為什么下載成功要將url從失敗數(shù)組中移除卒蘸。
因為[self.failedURLs addObject:url]是只在下載失敗時添加的,而下載成功和下載失敗是互斥的恰起,也就是說趾牧,下載成功時failedURLs數(shù)組里就不應(yīng)該有這個url,為什么要這么寫呢翘单,這是為了解決競態(tài)條件下的問題,若兩個線程下載同一個url的圖片貌亭,若第一個線程下載失敗认臊,第二個下載成功。如果不從failedURLs移除這個url的話,以后下載此url的圖片都會失敗斟薇。
- 為什么要返回返回的SDWebImageCombinedOperation類型
這個類型包含NSOperation *cacheOperation的一個子類型恕酸,其中cacheOperation中又存在id <SDWebImageOperation>的下載圖片的subOperation。在cancel的時候也應(yīng)該把這兩個操作都cancle蕊温。