寫在前面
SDWebImage是一個強大的圖片下載庫,提供的主要功能有:圖片異步下載,圖片緩存,圖片解碼以及其他確保程序健壯性的功能。其Github地址戳這里。
SDWebImage Class Diagram UML類圖
SDWebImage官方提供了這個開源庫的類圖。
為了方便閱讀在上面標(biāo)注了不同箭頭的關(guān)系。框架下各個模塊之間的關(guān)系都展示的很清楚了,就不再一一解釋。舉個例子,通常使用到的
UIImageView
的WebCache
分類下sd_setImageWithURL()
方法依賴于UIView
的WebCache
分類下的sd_internalSetImageWithURL()
方法的實現(xiàn)。而UIView
的WebCache
分類又依賴于SDWebImageManager
模塊。第一篇文章則是從這個角度入手分析SDWebImage的下載流程。
SDWebImage Sequence Diagram 流程圖
官方提供的流程圖如下:
清晰明了。
源碼分析
SDWebImageManager
SDWebImageManager是封裝好的一個單例,通過
+ (nonnull instancetype)sharedManager;
方法獲取。
SDWebImageManager是用于支撐WebCache
分類(如UIImageView+WebCache
)實現(xiàn)的一個類,并且連接了SDWebImageDownloader
異步下載器和SDImageCache
緩存模塊。
因此,在SDWebImageManager中封裝了以下幾個重要屬性:
//代理
@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;
//緩存類
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
//異步下載器
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;
代理對象中有兩個重要方法(但是是@optional的)在下面的代碼中將會使用到:
/**
* Controls which image should be downloaded when the image is not found in the cache.
*
* @param imageManager The current `SDWebImageManager`
* @param imageURL The url of the image to be downloaded
*
* @return Return NO to prevent the downloading of the image on cache misses. If not implemented, YES is implied.
*/
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;
/**
* Allows to transform the image immediately after it has been downloaded and just before to cache it on disk and memory.
* NOTE: This method is called from a global queue in order to not to block the main thread.
*
* @param imageManager The current `SDWebImageManager`
* @param image The image to transform
* @param imageURL The url of the image to transform
*
* @return The transformed image object.
*/
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;
- 第一個方法用于詢問代理在查找不到緩存的情況下是否需要根據(jù)url下載圖片,默認(rèn)返回YES。如果返回NO,即使緩存未命中,也不執(zhí)行下載操作。
- 第二個方法用于詢問代理是否需要對下載的圖像進行transform操作,然后緩存transform之后的圖片。如果代理實現(xiàn)了這個方法,則需要返回一張圖片。
WebCache
分類中的sd_setImageWithURL
最終都會調(diào)用SDWebImageManager
類中的
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options :(nullable SDWebImageDownloaderProgressBlock) progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock;
進行圖片的請求。
代碼注釋中有對該方法中傳入的幾個參數(shù)做出的說明,其中需要注意傳入的SDInternalCompletionBlock
參數(shù)的作用。
/**
* Downloads the image at the given URL if not present in cache or return the cached version otherwise.
*
* @param url The URL to the image
* @param options A mask to specify options to use for this request
* @param progressBlock A block called while image is downloading
* @note the progress block is executed on a background queue
* @param completedBlock A block called when operation has been completed.
*
* This parameter is required.
*
* This block has no return value and takes the requested UIImage as first parameter and the NSData representation as second parameter.
* In case of error the image parameter is nil and the third parameter may contain an NSError.
*
* The forth parameter is an `SDImageCacheType` enum indicating if the image was retrieved from the local cache
* or from the memory cache or from the network.
*
* The fifth parameter is set to NO when the SDWebImageProgressiveDownload option is used and the image is
* downloading. This block is thus called repeatedly with a partial image. When image is fully downloaded, the
* block is called a last time with the full image and the last parameter set to YES.
*
* The last parameter is the original image URL
*
* @return Returns an NSObject conforming to SDWebImageOperation. Should be an instance of SDWebImageDownloaderOperation
*/
SDWebImageOptions
是一個枚舉類型,里面存放了一些用戶可以自定義的圖片下載/緩存選項,在代碼中有用到的話再專門解釋其含義。
loadImageWithURL()方法
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options :(nullable SDWebImageDownloaderProgressBlock) progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock{
// 1. 判斷傳入的url合法性
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 將url設(shè)置為nil繼續(xù)執(zhí)行后續(xù)操作,防止程序崩潰
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
//初始化operation
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
//2. 判斷url是否在failedURLs中
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;
}
//將operation添加到數(shù)組中,任務(wù)完成后將被移除(后面會提到)
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
//3. 根據(jù)url獲取key
NSString *key = [self cacheKeyForURL:url];
//4. 請求緩存
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (operation.isCancelled) {
//4.1 操作已取消
[self safelyRemoveOperationFromRunning:operation];
return;
}
//需要更新已在緩存中的圖片 || 未獲取到緩存圖片
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
//4.2 options 中的 SDWebImageRefreshCached位為1 || 未獲取到緩存圖片
//用于處理來自同一個url的圖片,但是服務(wù)器中的圖片已經(jīng)更新,因此需要強制下載并更新緩存圖片。
if (cachedImage && options & SDWebImageRefreshCached) {
//緩存中已存在舊圖片,執(zhí)行回調(diào)通知。
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
//繼續(xù)執(zhí)行下載任務(wù)
SDWebImageDownloaderOptions downloaderOptions = 0;
//省略初始化downloaderOptions代碼
if (cachedImage && options & SDWebImageRefreshCached) {
//更新downloaderOptions
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
//使用更新的downloaderOptions開啟下載圖片任務(wù)
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) {
//error handling 錯誤處理
/*執(zhí)行回調(diào)將error傳出并將url放入failedURLs數(shù)組中*/
//代碼省略
}
else {
if ((options & SDWebImageRetryFailed)) {
/*options的SDWebImageRetryFailed位為1*/
/*默認(rèn)情況下當(dāng)url無法下載時,會添加到failedURLs數(shù)組中防止反復(fù)嘗試通過該url下載圖片,而如果該位為1,則該機制失效。*/
@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
//圖片刷新命中NSURLCache的情況 && downloadedImage為空
} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
//未命中NSURLCache且downloadedImage不為空
//options的SDWebImageTransformAnimatedImage位為1情況
//此時說明圖片需要執(zhí)行transform操作
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//Allows to transform the image immediately after it has been downloaded and just before to cache it on disk and memory.
//此時調(diào)用代理方法獲取transformed image
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];
}
//執(zhí)行回調(diào)
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
if (downloadedImage && finished) {
//一般情況:即以上出現(xiàn)的options位為0且獲取到下載的圖片
//根據(jù)cacheOnDisk緩存downloadedImage
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
//執(zhí)行回調(diào)
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
//通過原子操作移除下載
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
//給cancelBlock輔助,注意這段代碼不是在這里執(zhí)行的
operation.cancelBlock = ^{
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
}else if (cachedImage) {
//4.3 請求到緩存圖片 && !SDWebImageRefreshCached
//直接回調(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 {
// 4.4 圖片沒有在緩存中 && 代理沒有允許下載操作
__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;
}
方法內(nèi)部的注釋詳細(xì)描述了SDWebImageManager的loadImageWithURL
工作流程。概括起來其實很簡單:首先判斷緩存中是否能獲取到圖片,如果沒獲取到則使用異步下載器下載,然后使用緩存類緩存,執(zhí)行回調(diào)返回圖片。如果緩存中能夠獲取到,執(zhí)行回調(diào)返回緩存中的圖片。
下一篇文章將著重分析SDWebImage的緩存實現(xiàn)。