iOS | SDWebImage 介紹 & 源碼解析

簡(jiǎn)介

SDWebImage 是目前使用最廣泛的第三方圖片處理庫(kù)温算,它不僅能夠異步加載網(wǎng)絡(luò)圖片,還提供了一套圖片緩存管理機(jī)制间影,功能非常強(qiáng)大注竿。我們使用只要一行代碼就可以輕松的實(shí)現(xiàn)圖片異步下載和緩存功能。

image.png

本文主要針對(duì)v4.0.0版本進(jìn)行分析魂贬, 從3.0升級(jí)4.0的小伙伴可以查看 SDWebImage 4.0遷移指南巩割;

特性

  • 提供UIImageViewUIButton的分類, 支持網(wǎng)絡(luò)圖片的加載與緩存管理
  • 一個(gè)異步的圖片下載器
  • 異步(內(nèi)存+磁盤)緩存和自動(dòng)過期處理緩存
  • 后臺(tái)圖片解壓縮
  • 同一個(gè) URL 不會(huì)重復(fù)下載
  • 自動(dòng)識(shí)別無效 URL付燥,不會(huì)反復(fù)重試
  • 不阻塞主線程
  • 高性能
  • 使用 GCDARC

支持的圖像格式

  • 支持UIImage(JPEG宣谈,PNG,...)键科,包括GIF(從4.0版本開始,依靠FLAnimatedImage來處理動(dòng)畫圖像)
  • WebP格式闻丑,包括動(dòng)畫WebP(使用WebPsubspec

使用

UIImageView+WebCache
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]];;

這是UIImageView的一個(gè)擴(kuò)展方法漩怎,通過這個(gè)方法就已經(jīng)實(shí)現(xiàn)了 圖片異步加載緩存機(jī)制功能了, 是不是超簡(jiǎn)單呢


加載流程

首先: 我們通過時(shí)序圖,來了解一下框架的基本加載流程~

SDWebImageSequenceDiagram.png

  1. 當(dāng)我們的UIImageView嗦嗡、UIButton控件調(diào)用sd_setImageWithURL: ()...方法 來進(jìn)行加載圖片勋锤;
  2. 框架會(huì)直接調(diào)用 UIView+WebCache中的sd_internalSetImageWithURL:() ..., 該方法是UIImageViewUIButton 的共有拓展方法
  3. 接下來調(diào)用SDWebImageManager類中的loadImageWithURL:() ...方法,會(huì)根據(jù)提供的圖片URL 加載圖片侥祭,SDWebImageManager 主要負(fù)責(zé)管理SDImageCache緩存和SDWebImageDownloader下載器
  4. 首先進(jìn)入 SDImageCache類叁执,調(diào)用 queryCacheOperationForKey...在內(nèi)存或者磁盤進(jìn)行查詢,如果有圖片緩存則進(jìn)行回調(diào)展示卑硫, 如果沒有查詢到圖片緩存徒恋,則進(jìn)行下一步下載
  5. 在未查詢到圖片緩存時(shí), SDWebImageDownloader類會(huì)進(jìn)行網(wǎng)絡(luò)下載欢伏,下載成功后進(jìn)行回調(diào)展示入挣,并將下載的圖片緩存到內(nèi)存和磁盤

通俗理解: 根據(jù)Url內(nèi)存中查詢圖片,如果有則展示硝拧,沒有則在磁盤查詢圖片径筏,查詢到展示, 沒有查詢到在會(huì)通過網(wǎng)絡(luò)下載進(jìn)行展示障陶。下載完后會(huì)存儲(chǔ)到內(nèi)存和磁盤滋恬,方便下次直接使用,磁盤查詢和網(wǎng)絡(luò)下載都是異步的,不會(huì)影響主線程.

源碼分析

UIView+WebCache 加載邏輯

我們了解了基本的加載流程后抱究,接下來我們看看他是如何一步一步實(shí)現(xiàn)的,首先我們來看一下
UIView+WebCache.m文件中的加載圖片源碼:

/**
 @param url            圖片URL
 @param placeholder    占位圖
 @param options        加載選項(xiàng)(SDWebImageOptions枚舉類型)
 @param operationKey   要用作操的key鍵恢氯。如果為nil,將使用類名
 @param setImageBlock  自定義Block塊
 @param progressBlock  下載進(jìn)度Block塊
 @param completedBlock 加載完成Block塊
 */
- (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 {

    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);

    //  這個(gè)方法很強(qiáng)大,用于保障當(dāng)前的加載操作,不會(huì)被之前的加載操作污染(會(huì)將之前的加載操作取消)
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //  占位圖設(shè)置
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    if (url) {
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        __weak __typeof(self)wself = self;
        
        //  通過 SDWebImageManager類來進(jìn)行圖片加載
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
    
            __strong __typeof (wself) sself = wself;
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            //  會(huì)判斷是否是否在主線程,如果在則直接回調(diào),不在則執(zhí)行主線程異步回調(diào)
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                
                //  如果選項(xiàng)為 SDWebImageAvoidAutoSetImage(用戶手動(dòng)設(shè)置), 則Block回調(diào).不會(huì)給ImageView賦值,由用戶自己操作
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image)
                {
                    //  判斷是UIimageView還是UIButton給對(duì)應(yīng)的控件設(shè)置圖像
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    //  刷新視圖布局
                    [sself sd_setNeedsLayout];
                } else {
                    //  在加載網(wǎng)絡(luò)圖片失敗后,展示展位圖
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                //  加載完成回調(diào)
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //  將 operation添加到 operation字典中
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    }
    else
    {
        //  url為nil 則直接進(jìn)行錯(cuò)誤回調(diào)提示
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

UIView+WebCacheUIImageView提供了方法擴(kuò)展鼓寺,上面方法作為加載圖片的入口勋拟,邏輯也簡(jiǎn)單清晰;

視圖每次再加載圖片之前, 都會(huì)調(diào)用 sd_cancelImageLoadOperationWithKey:(nullable NSString *)key 方法, 用于取消上一個(gè)URl的加載操作, (比如: TalbeViewCell中的ImageView一般都是復(fù)用的,如果快速滑動(dòng), 這個(gè)ImageView會(huì)加載很多次不同的URL, 這個(gè)方法可以保證展示和加載的URL地址是最新的妈候,并且會(huì)將之前的操作取消)敢靡;
這個(gè)方法是UIView+WebCacheOperation類中提供的一個(gè)方法,我們看下具體實(shí)現(xiàn)

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    
    // SDOperationsDictionary是一個(gè)字典類型, key是當(dāng)前視圖的字符串, value是視圖加載的操作
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    
    //  通過key獲取到加載操作,operations實(shí)際是一個(gè) SDWebImageCombinedOperation類型,并遵守了<SDWebImageOperation>協(xié)議
    id operations = operationDictionary[key];
    //  將操作取消掉
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel]; //在協(xié)議方法中實(shí)現(xiàn)取消操作
        }
        //  并從字典中將操作移除
        [operationDictionary removeObjectForKey:key];
    }
}

每個(gè)視圖內(nèi)部會(huì)有一個(gè)SDOperationsDictionary 字典, key是當(dāng)前視圖的字符串, value是視圖加載的操作, 這個(gè)操作類遵守<SDWebImageOperation>協(xié)議, 在協(xié)議方法中實(shí)現(xiàn)真正的取消操作

dispatch_main_async_safe(^{})宏定義

#ifndef dispatch_main_async_safe        
#define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

該宏可以判斷是否是在主線程,如果在主線程則直接異步執(zhí)行block, 如果不在則調(diào)用 dispatch_async(dispatch_get_main_queue(), block);回主線程異步執(zhí)行;

SDWebImageManager

SDWebImageManager是一個(gè)單例管理類苦银,主要用于管理SDWebImageDownloader以及SDImageCache啸胧;SDImageCache類負(fù)責(zé)執(zhí)行圖片的緩存,SDWebImageDownloader類負(fù)責(zé)圖片的下載任務(wù)幔虏。該對(duì)象將一個(gè)下載器和一個(gè)圖片緩存綁定在一起纺念,并對(duì)外提供兩個(gè)只讀屬性來獲取它們,

@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;

我們看到他有個(gè)內(nèi)部有一個(gè) id<SDWebImageManagerDelegate> delegate屬性想括,SDWebImageManagerDelegate聲明如下:

// 緩存中沒有圖片的話是否下載這張圖片陷谱,如果返回NO的話,則如果緩存中沒有這張圖片主胧,也不會(huì)去重新下載
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
// 允許在圖片已經(jīng)被下載完成且被緩存到磁盤或內(nèi)存前立即轉(zhuǎn)換
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

如果我們要設(shè)置 只加載緩存叭首,不加載網(wǎng)絡(luò)圖片,或者在拿到圖片做一些處理踪栋,可以遵守改協(xié)議焙格,自己實(shí)現(xiàn)加載邏輯;

SDWebImageManager.m頁(yè)面同樣還有一個(gè) SDWebImageCombinedOperation類夷都,并遵守<SDWebImageOperation>協(xié)議眷唉,主要負(fù)責(zé)取消一些未執(zhí)行的NSOperation操作和正在執(zhí)行的操作

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;

@end

協(xié)議方法實(shí)現(xiàn):

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        _cancelBlock = nil;
    }
}

這個(gè)就是文章一開始,每次視圖加載前都需要取消之前的操作所執(zhí)行的方法囤官,通過cancelled屬性來中斷正在進(jìn)行的操作

下面我們來看下:SDWebImageManagerloadImageWithURL: options: progress: completed...加載圖片源碼具體實(shí)現(xiàn):

/*
    加載圖片,返回緩存版本冬阳。 如果不在緩存中,則使用給的URL下載圖像
 */
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
    
    // 如果是NSString類型則進(jìn)行強(qiáng)轉(zhuǎn)NSurl
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    // 防止應(yīng)用程序在參數(shù)類型錯(cuò)誤上崩潰
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    // 自定義Operation對(duì)象(繼承NSObject) 內(nèi)部有一個(gè) NSOperation對(duì)象
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    //  檢查是否為Url是否是失效的
    if (url) {
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }
    //  如果url為空或者是失效的,則直接block回調(diào),并附加上錯(cuò)誤信息,不再進(jìn)行加載操作
    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;
    }
    //  將當(dāng)前操作 添加 到正在運(yùn)行的操作  集合中
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    //  url轉(zhuǎn)字符串 Key
    NSString *key = [self cacheKeyForURL:url];

    //  并在緩存中查詢 圖片,如果內(nèi)存中有,直接回調(diào), 內(nèi)存中沒有,則會(huì)創(chuàng)建NSoperation操作,異步在 磁盤查詢
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        
        //  判斷操作是否被取消了
        if (operation.isCancelled) {
            
            //  從正在執(zhí)行的操作中移除,并renturn操作
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
        //  沒有圖片緩存  或者 options 為 SDWebImageRefreshCached 党饮,或者代理設(shè)置需要網(wǎng)絡(luò)加載肝陪,從網(wǎng)絡(luò)刷新
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            
            //  如果有緩存,但是提供了SDWebImageRefreshCached刑顺,先展示之前的緩存
            if (cachedImage && options & SDWebImageRefreshCached) {
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }
            
            // 根據(jù)SDWebImageOptions設(shè)置下載選項(xiàng)的 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;
            
            if (cachedImage && options & SDWebImageRefreshCached) {
                //如果圖像已經(jīng)緩存但強(qiáng)制刷新氯窍,則強(qiáng)制關(guān)閉
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                //忽略從NSURLCache讀取的圖像,如果緩存了圖像蹲堂,則強(qiáng)制刷新
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }   
            //  使用給定的URL創(chuàng)建SDWebImageDownloader加載器實(shí)例
            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) {
                    // 如果Operation被取消
                }
                else if (error)
                {
                    // 錯(cuò)誤回調(diào)
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) { 
                        //  加入黑名單
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else
                {
                    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];
                                // 如果圖像被轉(zhuǎn)換狼讨,傳遞nil,這樣我們就可以重新計(jì)算圖像中的數(shù)據(jù)
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            } 
                            [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];
                        }
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }
                if (finished) {
                    //  移除 Operation操作
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            operation.cancelBlock = ^{
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self safelyRemoveOperationFromRunning:strongOperation];
            };  
        }
        else if (cachedImage)   // 從內(nèi)存或者磁盤中查詢到圖片信息
        {
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            //  進(jìn)行Block回調(diào)
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            //  從正在運(yùn)行的集合中移除 移除 operation操作
            [self safelyRemoveOperationFromRunning:operation];
        }
        else
        {
            __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)部會(huì)創(chuàng)建一個(gè) SDWebImageCombinedOperation對(duì)象政供,這個(gè)對(duì)象用于貫穿緩存查詢和下載操作,如果operation.isCancelledYES朽基,則回調(diào)操作會(huì)直接 return布隔,不在繼續(xù)執(zhí)行;

方法內(nèi)部首先會(huì)調(diào)用了[self.imageCache queryCacheOperationForKey:()...]方法踩晶,用于查詢圖片緩存执泰,如果沒有緩存則會(huì)調(diào)用[self.imageDownloader downloadImageWithURL:()...] 下載方法, 在獲得到圖片后,會(huì)將圖片通過Block回調(diào)傳給視圖。

SDImageCache緩存類

SDImageCache是一個(gè)全局單例類, 負(fù)責(zé)處理內(nèi)存緩存及磁盤緩存渡蜻。其中磁盤緩存的讀寫操作是異步的术吝,這樣就不會(huì)對(duì)UI操作造成影響。在SDImageCache內(nèi)部有一個(gè)NSCache屬性,用于處理內(nèi)存緩存, NSCache是一個(gè)類似NSDictionary一個(gè)可變的集合,當(dāng)內(nèi)存警告時(shí)內(nèi)部自動(dòng)清理部分緩存數(shù)據(jù)茸苇。磁盤緩存的處理則是使用NSFileManager對(duì)象來實(shí)現(xiàn)的排苍。圖片存儲(chǔ)的位置是位于Cache文件夾。另外学密,SDImageCache還定義了一個(gè)串行隊(duì)列淘衙,來異步存儲(chǔ)圖片。

內(nèi)存緩存與磁盤緩存相關(guān)變量的聲明及定義如下:

@interface SDImageCache ()
@property (strong, nonatomic, nonnull) NSCache *memCache;                      
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;  
@end
@implementation SDImageCache {
    NSFileManager *_fileManager;
}              
// 初始化
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

我們看下SDImageCache查詢緩存源碼:

//  異步查詢緩存并在完成時(shí)調(diào)用完成的操作腻暮。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    
    //  key是空的則直接回調(diào),并返回nil
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // 首先檢查內(nèi)存緩存…
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    
    //  如果內(nèi)存中有圖片
    if (image) {
        NSData *diskData = nil;
        //  判斷是否為gif圖
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        //  回調(diào)內(nèi)存圖片信息
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }

    // 內(nèi)存未查到圖片信息 則創(chuàng)建一個(gè) operation
    NSOperation *operation = [NSOperation new];
    
    //  在一個(gè)串行里異步執(zhí)行
    dispatch_async(self.ioQueue, ^{
        
         // 如果取消了彤守,則進(jìn)行回調(diào)
        if (operation.isCancelled) {
            return;
        }
        // 從磁盤進(jìn)行查詢
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            
            //  判斷是否有圖片并需要內(nèi)存緩存
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                //  內(nèi)存緩存一份
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            
            //  進(jìn)行回調(diào)
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

通過上面的代碼,我們可以看出, 首先在內(nèi)存中查詢圖片通過imageFromMemoryCacheForKey:() ...

//  從內(nèi)存中查看是否有改圖片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    return [self.memCache objectForKey:key];
}

key為圖片的 URL 地址毯侦, Value則是緩存圖片,如果內(nèi)存沒有緩存,則會(huì)調(diào)用[self diskImageDataBySearchingAllPathsForKey:key]進(jìn)行磁盤查詢

//  磁盤查詢
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }

    // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
    // checking the key with and without the extension
    data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
    if (data) {
        return data;
    }
    NSArray<NSString *> *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }
        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension];
        if (imageData) {
            return imageData;
        }
    }
    return nil;
}

如果內(nèi)存和磁盤都未查詢到圖片,則會(huì)進(jìn)行網(wǎng)絡(luò)請(qǐng)求下載圖片

另外SDImageCache還提供了存儲(chǔ)圖片

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    
    if (!image || !key) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // 內(nèi)存緩存
    if (self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    // 如果磁盤緩存
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{ // 在一個(gè)串行隊(duì)列里面進(jìn)行
            NSData *data = imageData;
            
            if (!data && image) {
                SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];   // 獲取圖片格式
                data = [image sd_imageDataAsFormat:imageFormatFromData];  //根據(jù)格式返回二進(jìn)制數(shù)據(jù)
            }
            
            [self storeImageDataToDisk:data forKey:key];    // 存儲(chǔ)到磁盤
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

緩存操作具垫,該操作會(huì)在內(nèi)存中放置一份緩存侈离,而如果確定需要緩存到磁盤,則將磁盤緩存操作作為一個(gè)task放到串行隊(duì)列中處理筝蚕。在方法中卦碾,會(huì)先檢測(cè)圖片是PNG還是JPEG,并將其轉(zhuǎn)換為相應(yīng)的圖片數(shù)據(jù)起宽,最后將數(shù)據(jù)寫入到磁盤中(文件名是對(duì)key值做MD5后的串)洲胖。

SDWebImageDownloader下載管理器

SDWebImageDownloader下載管理器是一個(gè)單例類,它主要負(fù)責(zé)圖片的下載的管理坯沪。實(shí)質(zhì)的下載操作則是通過SDWebImageDownloaderOperation類繼承NSOperation绿映,圖片的下載操作是放在一個(gè)NSOperationQueue操作隊(duì)列中來完成的。

@property (strong, nonatomic) NSOperationQueue *downloadQueue;

默認(rèn)情況下屏箍,隊(duì)列最大并發(fā)數(shù)是6绘梦。如果需要的話,我們可以通過SDWebImageDownloader類的maxConcurrentDownloads屬性來修改赴魁。

我看來看下downloadImageWithURL:()..options:(): progress():completed:()下載操作內(nèi)部實(shí)現(xiàn)

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;
    
    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        //  默認(rèn)15秒
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        //  為了防止?jié)撛诘闹貜?fù)緩存(NSURLCache + SDImageCache)卸奉,如果被告知其他信息,我們將禁用圖像請(qǐng)求的緩存
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        //  創(chuàng)建下載操作
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        //  是否解碼
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        
        // 操作加入隊(duì)列
        [sself.downloadQueue addOperation:operation];
        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;
    }];
}

該方法的內(nèi)部會(huì)調(diào)用addProgressCallback..方法颖御,會(huì)將圖片下載的一些回調(diào)信息存儲(chǔ)在SDWebImageDownloaderOperation類的URLCallbacks屬性中榄棵,該屬性是一個(gè)字典,key是圖片的URL地址潘拱,value則是一個(gè)數(shù)組疹鳄,包含每個(gè)圖片的多組回調(diào)信息。

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    
    // URL將用作回調(diào)字典的鍵芦岂,因此它不能為nil瘪弓。如果是nil,立即調(diào)用完整的塊禽最,沒有圖像或數(shù)據(jù)腺怯。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    
    __block SDWebImageDownloadToken *token = nil;
    
    //  按順序依次執(zhí)行
    dispatch_barrier_sync(self.barrierQueue, ^{
        
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        
        if (!operation) {
            // 創(chuàng)建下載 SDWebImageDownloaderOperation操作
            operation = createCallback();
            
            //  添加到URLOperations操作緩存中
            self.URLOperations[url] = operation;
            
            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
                
                SDWebImageDownloaderOperation *soperation = woperation;
                if (!soperation) return;
                if (self.URLOperations[url] == soperation) {
                    //  下載完移除下載操作
                    [self.URLOperations removeObjectForKey:url];
                };
            };
        }
        //  將一些回到信息放入`SDWebImageDownloaderOperation`類的`callbackBlocks`屬性中,并創(chuàng)建 downloadOperationCancelToken實(shí)例
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        
        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });
    
    return token;
}

SDWebImageDownloaderOperation對(duì)象加入到操作隊(duì)列后川无,就開始調(diào)用該對(duì)象的start方法呛占,代碼如下

// SDWebImageDownloaderOperation.m
- (void)start {
    // 如果操作被取消,就reset設(shè)置
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

        ...
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            // 創(chuàng)建session的配置
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            // 創(chuàng)建session對(duì)象
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    // 開始下載任務(wù)
    [self.dataTask resume];

    if (self.dataTask) {
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    } else {
        // 創(chuàng)建任務(wù)失敗
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }
    ...
}

在下載過程中懦趋,會(huì)涉及鑒權(quán)晾虑、響應(yīng)的statusCode判斷(404304等等),以及收到數(shù)據(jù)后的進(jìn)度回調(diào)等等帜篇,在最后的URLSession:task:didCompleteWithError里做最后的處理糙捺,然后回調(diào)完成的block,下面僅分析一下- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error的方法

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    ...
    if (error) {
        [self callCompletionBlocksWithError:error];
    } else {
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            /**
             *  See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
             *  Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
             *    and images for which responseFromCached is YES (only the ones that cannot be cached).
             *  Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
             */
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
                // 如果options是忽略緩存笙隙,而圖片又是從緩存中取的继找,就給回調(diào)傳入nil
                [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
            } else if (self.imageData) {
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                // 緩存圖片
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                // 跳轉(zhuǎn)圖片的大小
                image = [self scaledImageForKey:key image:image];
                
                // Do not force decoding animated GIFs
                if (!image.images) {
                    // 不是Gif圖像
                    if (self.shouldDecompressImages) {
                        if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
                            image = [UIImage decodedAndScaledDownImageWithImage:image];
                            [self.imageData setData:UIImagePNGRepresentation(image)];
#endif
                        } else {
                            image = [UIImage decodedImageWithImage:image];
                        }
                    }
                }
                if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                    // 下載是圖片大小的0
                    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                } else {
                    // 把下載的圖片作為參數(shù)回調(diào)
                    [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
                }
            } else {
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
            }
        }
    }
    ...
}
以上是個(gè)人對(duì)SDWebImage加載圖片流程的理解,如有錯(cuò)誤之處逃沿,還望各路大俠給予指出!
SDWebImage源碼地址: https://github.com/SDWebImage/SDWebImage
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末幻锁,一起剝皮案震驚了整個(gè)濱河市凯亮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哄尔,老刑警劉巖假消,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異岭接,居然都是意外死亡富拗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門鸣戴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來啃沪,“玉大人,你說我怎么就攤上這事窄锅〈辞В” “怎么了?”我有些...
    開封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵入偷,是天一觀的道長(zhǎng)追驴。 經(jīng)常有香客問我,道長(zhǎng)疏之,這世上最難降的妖魔是什么殿雪? 我笑而不...
    開封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮锋爪,結(jié)果婚禮上丙曙,老公的妹妹穿的比我還像新娘。我一直安慰自己几缭,他們只是感情好河泳,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拆挥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惰瓜。 梳的紋絲不亂的頭發(fā)上汉矿,一...
    開封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音洲拇,去河邊找鬼。 笑死男翰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蛾绎。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼鸦列,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼租冠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起薯嗤,我...
    開封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤顽爹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后骆姐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體话原,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年诲锹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了繁仁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡归园,死狀恐怖黄虱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情庸诱,我是刑警寧澤捻浦,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站桥爽,受9級(jí)特大地震影響朱灿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜钠四,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一盗扒、第九天 我趴在偏房一處隱蔽的房頂上張望跪楞。 院中可真熱鬧,春花似錦侣灶、人聲如沸甸祭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)凡怎。三九已至,卻和暖如春统倒,著一層夾襖步出監(jiān)牢的瞬間檐薯,已是汗流浹背坛缕。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工赚楚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宠页,地道東北人举户。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓俭嘁,卻偏偏與公主長(zhǎng)得像供填,于是被迫代替她去往敵國(guó)和親罢猪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子膳帕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容