SDWebImage是一個(gè)開(kāi)源的第三方庫(kù),它提供了UIImageView的分類來(lái)實(shí)現(xiàn)從網(wǎng)絡(luò)端下載數(shù)據(jù)并緩存到內(nèi)存和磁盤(pán)。
SDWebImage有如下特點(diǎn):
- 提供了UIImageView和UIButton的分類。以支持加載網(wǎng)絡(luò)圖片并緩存。
- 一個(gè)異步的圖片下載器
- 提供異步的內(nèi)存和磁盤(pán)緩存,并自動(dòng)處理緩存過(guò)期。
- 后臺(tái)圖片解壓縮處理。
- 確保同一個(gè)URL不會(huì)被下載多次。
- 確保主線程永遠(yuǎn)不會(huì)阻塞。
一.儲(chǔ)備知識(shí)
SDWebImage中每一個(gè)下載任務(wù)都是一個(gè)SDWebImageDownloaderOperation
,而SDWebImageDownloaderOperation
又是繼承自NSOperation
,所以每一個(gè)下載任務(wù)對(duì)應(yīng)一個(gè)NSOperation
。在SDWebImage
中使用SDWebImageDownloader
來(lái)管理
多個(gè)下載任務(wù),在SDWebImageDownloader
中有一個(gè)downloadedQueue
這個(gè)屬性,這個(gè)屬性是NSOperationQueue
類型的,也就是用NSOperationQueue
來(lái)管理NSOperation
。
下面我們就一起學(xué)習(xí)一下NSOperation
和NSOperationQueue
。
NSOperation和NSOperationQueue
NSOperation是一個(gè)抽象類,用來(lái)表示與單個(gè)任務(wù)相關(guān)聯(lián)的代碼和數(shù)據(jù)。
NSOperation類是一個(gè)抽象類,我們不能直接去使用它而是應(yīng)該創(chuàng)建一個(gè)子類來(lái)繼承它。雖然它只是一個(gè)抽象類,但是它的基本實(shí)現(xiàn)還是提供了很有價(jià)值的邏輯來(lái)確保任務(wù)的安全執(zhí)行。這種原生的邏輯可以讓我們專注于任務(wù)的真正的實(shí)現(xiàn),而不需要用一些膠水代碼去確保這個(gè)任務(wù)能正常工作在其他地方。
我們可以把一個(gè)NSOperation對(duì)象加入到一個(gè)operation queue中,讓這個(gè)operation queue去決定什么時(shí)候執(zhí)行這個(gè)operation。當(dāng)使用operation queue去管理operation時(shí),輪到某個(gè)operation執(zhí)行時(shí)實(shí)際是去執(zhí)行這個(gè)operation的start
方法,所以我們一個(gè)operation對(duì)象實(shí)際要執(zhí)行的任務(wù)應(yīng)該放在start方法里面。如果我們不想使用operation queue,也可以通過(guò)手動(dòng)調(diào)用NSOperation的start方法來(lái)執(zhí)行任務(wù)。
我們可以通過(guò)添加依賴來(lái)確定operation queue中operation的具體執(zhí)行順序。添加依賴和移除依賴可以使用下列的API:
//添加依賴
- (void)addDependency:(NSOperation *)op;
//移除依賴
- (void)removeDependency:(NSOperation *)op;
只要當(dāng)一個(gè)operation對(duì)象的所有依賴都執(zhí)行完成的時(shí)候,其才可以變成熟ready狀態(tài),然后才可以被執(zhí)行。如果一個(gè)operation沒(méi)有添加依賴,直接加入了operation queue中,那么就會(huì)按照加入隊(duì)列的先后順序,當(dāng)這個(gè)operation的前一個(gè)operation執(zhí)行完成以后,其狀態(tài)才會(huì)變成ready,才能被執(zhí)行。
NSOperation對(duì)象有下列四個(gè)比較重要的狀態(tài):
- isCancelled
- isExecuting
- isFinished
- isReady
其中isExecuting
,isFinished
,isReady
這三種狀態(tài)相當(dāng)于是operation對(duì)象的生命周期:
operation生命周期.png
而isCancelled
這種狀態(tài)則比較特殊,當(dāng)我們對(duì)operation對(duì)象調(diào)用- (void)cancel
方法時(shí),其isCancelled
屬性會(huì)被置為YES。這個(gè)時(shí)候要分兩種情況: - operation正在執(zhí)行
也就是說(shuō)其狀態(tài)現(xiàn)在是isExecuting
,調(diào)用- (void)cancel
方法后會(huì)馬上停止執(zhí)行當(dāng)前任務(wù),并且狀態(tài)變?yōu)?code>isFinished,isCancelled = Yes
。 - operation還沒(méi)開(kāi)始執(zhí)行
這個(gè)時(shí)候operation的狀態(tài)其實(shí)是isReady
這個(gè)狀態(tài)之前,operation還沒(méi)開(kāi)始執(zhí)行,調(diào)用- (void)cancel
方法后會(huì)去調(diào)用operation的start
方法,在start
方法里我們要去處理cancel事件,并設(shè)置isFinished = YES
。
調(diào)用cancel方法.png
SDWebImageOptions
在SDWebImage中大量使用了option類型,通過(guò)判斷option類型的值來(lái)決定下一步應(yīng)該怎么做,所以如果對(duì)這些option值一點(diǎn)都不了解的話可能理解起源碼來(lái)也會(huì)非常難受。SDWebImageOptions
是暴露在外的可供使用者使用的option。還有一些option比如SDImageCacheOptions
, SDWebImageDownloaderOptions
這些都是不暴露給用戶使用的。源碼中是根據(jù)用戶設(shè)置的SDWebImageOptions
這個(gè)option來(lái)確定另外兩個(gè)option的值。
下面我們來(lái)具體看一下SDWebImageOptions
這里我們也可以看到,
SDImageCacheOptions
中的三個(gè)選項(xiàng)在SDWebImageOptions
中都有對(duì)應(yīng)的選項(xiàng)。
-
SDImageCacheQueryDataWhenInMemory
對(duì)應(yīng)SDWebImageQueryDataWhenInMemory
。 -
SDImageCacheQueryDiskSync
對(duì)應(yīng)SDWebImageQueryDiskSync
。 -
SDImageCacheScaleDownLargeImages
對(duì)應(yīng)SDWebImageScaleDownLargeImages
。
SDWebImageDownloaderOptions
SDWebImageDownloaderOptions
中所有選項(xiàng)在SDWebImageOptions
中也有相對(duì)應(yīng)的選項(xiàng),這里不再贅述。
SDImageCacheType
//從緩存中得不到數(shù)據(jù)
SDImageCacheTypeNone,
//從磁盤(pán)緩存中得到數(shù)據(jù)
SDImageCacheTypeDisk,
//從內(nèi)存緩存中得到數(shù)據(jù)
SDImageCacheTypeMemory
框架的主要類和一次圖片加載的主要流程
框架的主要類
一次圖片加載的主要流程
針對(duì)上圖中一次圖片加載的主要流程,每一步做介紹:
- 1.SDWebImage為UIImagView創(chuàng)建了一個(gè)分類
UIImageView (WebCache)
,然后UIImageView對(duì)象可以調(diào)用這個(gè)分類的方法來(lái)下載圖片:
[imageView sd_setImageWithURL:[NSURL URLWithString:@""]];
- 2.
UIImageView (WebCache)
的- (void)sd_setImageWithURL:(nullable NSURL *)url
方法實(shí)際調(diào)用了UIView (WebCache)
的下列方法:
- (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;
- 3.
UIView (WebCache)
的上述方法在實(shí)現(xiàn)時(shí)會(huì)創(chuàng)建一個(gè)SDWebImageManager
的實(shí)例對(duì)象,然后調(diào)用其下列方法來(lái)加載圖片:
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
- 4.在
SDWebImageManager
對(duì)象的上述方法里,首先會(huì)查詢?cè)诰彺嬷杏袥](méi)有這個(gè)圖片,然后根據(jù)各種option的判斷決定是否要從網(wǎng)絡(luò)端下載。查詢緩存中有沒(méi)有是通過(guò)調(diào)用SDImageCache
對(duì)象的實(shí)例方法來(lái)實(shí)現(xiàn)的:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key
options:(SDImageCacheOptions)options
done:(nullable SDCacheQueryCompletedBlock)doneBlock;
- 5.返回緩存查詢的結(jié)果
- 6.如果需要下載圖片,那么調(diào)用
SDWebImageDownloader
對(duì)象的下列方法進(jìn)行下載:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
- 7.獲取從網(wǎng)絡(luò)端下載的圖片。
- 8.判斷是否要將下載的圖片進(jìn)行緩存,如果需要,則緩存。
- 9.把通過(guò)
SDWebImageManager
對(duì)象獲取的圖片顯示在UIImageView
上。
源碼分析
這一部分我們進(jìn)行詳細(xì)的源碼分析。
首先從SDWebImageManager
類的loadImageWithURL:
方法看起:
由于代碼比較長(zhǎng),我就采用注釋的方式
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
//如果傳進(jìn)來(lái)的是一個(gè)NSString,則把NSString轉(zhuǎn)化為NSURL
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;
}
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
//self.failedURLs是nsurl的黑名單,一般情況下,如果URL在這個(gè)黑名單里,那么就不會(huì)嘗試加載這個(gè)圖片,直接返回
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
//SDWebImageRetryFailed即即使URL被加入了黑名單,也要嘗試加載這個(gè)URL對(duì)應(yīng)的圖片
//如果URL長(zhǎng)度為0,或者URL被加入了黑名單并且沒(méi)有設(shè)置SDWebImageRetryFailed,那么就直接回調(diào)完成的block
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;
}
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
NSString *key = [self cacheKeyForURL:url];
//由于我們?cè)谑褂肁PI的時(shí)候只設(shè)置SDWebImageOptions,所以這里就是根據(jù)用戶設(shè)置的SDWebImageOptions去設(shè)置SDImageCacheOptions
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
__weak SDWebImageCombinedOperation *weakOperation = operation;
//這里開(kāi)始調(diào)用SDImageCache對(duì)象的queryCacheOperationForKey:方法去緩存中查找有沒(méi)有這個(gè)URL對(duì)應(yīng)的圖片
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
// 判斷我們是否需要從網(wǎng)絡(luò)端下載圖片
//首先檢查沒(méi)有設(shè)置只能從緩存中獲取,然后檢查cachedImage = nil或者設(shè)置了要刷新緩存,則需要從網(wǎng)絡(luò)端下載圖片
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
if (shouldDownload) {
//從緩存中獲取了圖片并且設(shè)置了要刷新緩存這個(gè)option,則要進(jìn)行兩次完成的回調(diào),這是第一次回調(diào)
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:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// download if no image or requested to refresh anyway, and download allowed by delegate
//這里是根據(jù)用戶設(shè)置的SDWebImageOptions來(lái)手動(dòng)設(shè)置SDWebImageDownloaderOptions
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
//如果已經(jīng)從緩存中獲取了圖片并且設(shè)置了要刷新緩存
if (cachedImage && options & SDWebImageRefreshCached) {
//這里其實(shí)就是把SDWebImageDownloaderProgressiveDownload這個(gè)option去掉
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
//加上SDWebImageDownloaderIgnoreCachedResponse這個(gè)option,忽略NSURLCache中緩存的response
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
__weak typeof(strongOperation) weakSubOperation = strongOperation;
//l開(kāi)始進(jìn)行圖片的下載
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
if (!strongSubOperation || strongSubOperation.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
} else if (error) {
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL;
// 后面都是判斷在請(qǐng)求失敗的情況下是否應(yīng)該把
if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
} else {
shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost);
}
if (shouldBlockFailedURL) {
LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
UNLOCK(self.failedURLsLock);
}
}
else {
//如果設(shè)置了SDWebImageRetryFailed那么就要把URL從黑名單中移除
if ((options & SDWebImageRetryFailed)) {
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
//判斷是否應(yīng)該把下載的圖片緩存到磁盤(pán),SDWebImageCacheMemoryOnly這個(gè)option表示只把圖片緩存到內(nèi)存
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
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), ^{
@autoreleasepool {
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
});
} else {
//可以直接看到這一部分
if (downloadedImage && finished) {
if (self.cacheSerializer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
});
} else {
//對(duì)圖片進(jìn)行緩存
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
}
//第二次調(diào)用完成的block
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
}];
//如果從從緩存中獲取了圖片并且不需要下載
} else if (cachedImage) {
//執(zhí)行完成的回調(diào)
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
} else {
// 緩存中沒(méi)有獲取圖片,也不用下載
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
return operation;
}
總結(jié)一下SDWebImageManager
的loadImageWithURL:
所做的事情:
其實(shí)在
loadImageWithURL:
里面做了加載圖片的完整流程。首先檢查傳入的NSURL的有效性。然后開(kāi)始從緩存中查找是否有這個(gè)圖片,得到查詢結(jié)果之后再根據(jù)查詢結(jié)果和設(shè)置的option判斷是否需要進(jìn)行下載操作,如果不需要下載操作那么就直接使用cache image進(jìn)行下載回調(diào)。如果需要進(jìn)行下載操作那么就開(kāi)始下載,下載完成后按照設(shè)置的option將圖片緩存到內(nèi)存和磁盤(pán),最后進(jìn)行完成的回調(diào)。
然后我們看一下查詢緩存的具體過(guò)程,也就是SDImageCache
這個(gè)類的queryCacheOperationForKey:
方法:
這里也是采用注釋的方式
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 首先檢查內(nèi)存緩存中有沒(méi)有這個(gè)圖片,注意內(nèi)存緩存使用的是NSCache,它是一個(gè)類字典結(jié)構(gòu),使用圖片對(duì)應(yīng)的nsurl作為key,在查詢的時(shí)候就用這個(gè)key去查詢
UIImage *image = [self imageFromMemoryCacheForKey:key];
//是否只查詢內(nèi)存緩存(如果從內(nèi)存緩存中獲取了圖片并且沒(méi)有設(shè)置SDImageCacheQueryDataWhenInMemory,那么就只查詢內(nèi)存緩存)
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
//執(zhí)行回調(diào)
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
//從磁盤(pán)中查詢
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
//j緩存獲取的類型,有三種m類型,none,memory,disk
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// 圖片是從內(nèi)存緩存中獲取的
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
//圖片是從磁盤(pán)緩存中獲取的
cacheType = SDImageCacheTypeDisk;
// 解壓圖片
diskImage = [self diskImageForKey:key data:diskData options:options];
//判斷是否需要把圖片緩存到內(nèi)存
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
//將圖片緩存到內(nèi)存
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
總結(jié)一下queryCacheOperationForKey:
方法所做的事情:
SDImageCache
這個(gè)類是專門(mén)負(fù)責(zé)緩存相關(guān)的問(wèn)題的,包括查詢緩存和將圖片進(jìn)行緩存。SDImageCache
使用了一個(gè)NSCache
對(duì)象來(lái)進(jìn)行內(nèi)存緩存,磁盤(pán)緩存則是把圖片數(shù)據(jù)存放在應(yīng)用沙盒的Caches
這個(gè)文件夾下。
首先查詢內(nèi)存緩存,內(nèi)存緩存查詢完了以后再判斷是否需要查詢磁盤(pán)緩存。如果查詢內(nèi)存緩存已經(jīng)有了結(jié)果并且沒(méi)有設(shè)置一定要查詢磁盤(pán)緩存,那么就不查詢磁盤(pán)緩存,否則就要查詢磁盤(pán)緩存。內(nèi)存緩存沒(méi)有查詢到圖片,并且磁盤(pán)緩存查詢到了圖片,那么就要把這個(gè)內(nèi)容緩存到內(nèi)存緩存中。
圖片的緩存查詢完成后我們?cè)賮?lái)看一下下載操作,即SDWebImageDownloader
的downloadImageWithURL:
方法
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
// 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 nil;
}
LOCK(self.operationsLock);
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
if (!operation || operation.isFinished) {
//創(chuàng)建一下下載的operation
operation = [self createDownloaderOperationWithUrl:url options:options];
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
//把operation加入到nNSOperationQueue中去
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
//這一部分代碼是在取消operation的時(shí)候使用
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
SDWebImageDownloader
這個(gè)類是專門(mén)管理下載的,它有一個(gè)屬性是downloadQueue
,這是一個(gè)NSOperationQueue
,每創(chuàng)建一個(gè)新的下載任務(wù)都把它加入到這個(gè)downloadQueue
中,讓downloadQueue
去管理任務(wù)的開(kāi)始,取消,結(jié)束。
上面的方法其實(shí)做的事情很簡(jiǎn)單,就是創(chuàng)建了一個(gè)下載圖片的operation,然后把它加入到了
downloadQueue
中去。
下面我們來(lái)具體看一下創(chuàng)建下載圖片的operation的過(guò)程,即SDWebImageDownloader
類的createDownloaderOperationWithUrl:
方法:
- (NSOperation<SDWebImageDownloaderOperationInterface> *)createDownloaderOperationWithUrl:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options {
NSTimeInterval timeoutInterval = self.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (self.headersFilter) {
request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [self allHTTPHeaderFields];
}
//前面都是為了創(chuàng)建一個(gè)request,然后使用request和session對(duì)象去創(chuàng)建下載的operation
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
operation.shouldDecompressImages = self.shouldDecompressImages;
if (self.urlCredential) {
operation.credential = self.urlCredential;
} else if (self.username && self.password) {
operation.credential = [NSURLCredential credentialWithUser:self.username password:self.password persistence:NSURLCredentialPersistenceForSession];
}
//設(shè)置operation的隊(duì)列優(yōu)先級(jí)
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//如果設(shè)置的執(zhí)行順序是xLIFI,即后進(jìn)先出,則要把queue中的最后一個(gè)加入的operation的依賴設(shè)置為該operation,這樣來(lái)保證這個(gè)operation最先執(zhí)行
if (self.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[self.lastAddedOperation addDependency:operation];
self.lastAddedOperation = operation;
}
return operation;
}
這個(gè)方法就是創(chuàng)建了一個(gè)request對(duì)象,然后使用這個(gè)request對(duì)象和session對(duì)象去創(chuàng)建下載的operation對(duì)象。
我們看一下負(fù)責(zé)單個(gè)下載任務(wù)的operation對(duì)象到底是怎么創(chuàng)建的,即SDWebImageDownloaderOperation
類的- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session options:(SDWebImageDownloaderOptions)options
方法:
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options {
if ((self = [super init])) {
_request = [request copy];
_shouldDecompressImages = YES;
_options = options;
_callbackBlocks = [NSMutableArray new];
_executing = NO;
_finished = NO;
_expectedSize = 0;
_unownedSession = session;
_callbacksLock = dispatch_semaphore_create(1);
_coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
這個(gè)初始化方法其實(shí)也很簡(jiǎn)單,就是給自己的成員變量賦值
我們知道,NSOperation類的真正執(zhí)行任務(wù)是在其start方法里面,那么我們看一下SDWebImageDownloaderOperation
的start
方法的具體實(shí)現(xiàn):
代碼比較長(zhǎng),我在關(guān)鍵部分加了注釋
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if SD_UIKIT
//這一部分就是解決在后臺(tái)仍然進(jìn)行下載的問(wèn)題
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
NSURLSession *session = self.unownedSession;
//創(chuàng)建一個(gè)session對(duì)象,因?yàn)楹竺嬉獎(jiǎng)?chuàng)建NSURLSessionTask,需要session對(duì)象
if (!session) {
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.
*/
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
self.ownedSession = session;
}
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// Grab the cached data for later check
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
// NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
//創(chuàng)建dataTask,這個(gè)才是真正執(zhí)行下載任務(wù)的
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
}
}
#pragma clang diagnostic pop
[self.dataTask resume];
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
});
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
[self done];
return;
}
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
這里就是通過(guò)一個(gè)session對(duì)象和一個(gè)request對(duì)象創(chuàng)建了一個(gè)dataTask對(duì)象,這個(gè)dataTask對(duì)象才是真正用來(lái)下載的,然后調(diào)用
[self.dataTask resume]
執(zhí)行下載。
到這里SDWebImage的源碼分析就結(jié)束啦。
這篇文章在簡(jiǎn)書(shū)的地址:SDWebImage源碼解讀