SDWebImage原始碼閱讀(上)

周老實發表於2018-01-18

前言

SDWebImage
SDWebImage 想必對於每一個iOS開發者來說,都不會陌生。每當我們的專案中需要給UIImageView設定網路圖片的時候,我們基本上第一個想到就是這個強大的第三方庫。我們可以手動匯入靜態庫,也可以直接用CocoaPod管理,只要簡單地輸入pod 'SDWebImage'即可匯入,大家可以隨意選擇。 雖然在專案中經常用到這個庫,但是有時候我們可能只是知道用,卻並沒有去了解下這個強大庫實現原理,或者是寫這個庫的大佬程式碼思路、風格和各種技巧。這篇文章就是從SDWebImage一行行原始碼來分析大佬的神級操作。

SDWebImage流程示意圖

SDWebImage流程示意圖

從sd_setImageWithURL開始

在我們將SDWebImage匯入到專案中的時候,點開core資料夾可能會看到一大串檔案,剛開始肯定覺得很頭疼,這麼多檔案,這麼多檔案,我要看到什麼時候去,畏難心湧上來了,很多同學就在這裡倒下,把Xcode一關,然後幹別的事去了...。其實這樣很正常,不過我們可以整理下,其實SDWebImage真正核心的也就幾個檔案,我們可以把他們單獨拿出來,比如這樣

SDWebImage檔案整理

這樣可能會看得舒服點。把檔案整理好了,那我們就開始看下每個檔案有什麼用,幹什麼的了。我們先從最常用的方法開始:sd_setImageWithURL

我們隨便佈局一個UIImageView,然後設定圖片:

UIImageView *img = [[UIImageView alloc] init];
img.frame = CGRectMake(0, 0, 100, 100);
img.center = self.view.center;
img.backgroundColor = [UIColor orangeColor];
[self.view addSubview:img];
//載入圖片
[img sd_setImageWithURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1516084683&di=aeeadb9d3ae89483d06792970a46f829&imgtype=jpg&er=1&src=http%3A%2F%2Fp3.qhimg.com%2Ft010f0383e8e5f9bdbb.png"] placeholderImage:nil];
複製程式碼

我們可以點進這個方法看看是怎麼實現的:

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}
複製程式碼

然後一路點下去,你會發現,它們最終都會調到UIView+WebCache的sd_internalSetImageWithURL方法裡面來執行,我們看看這裡面的程式碼:

- (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 {
    //獲取到繫結的key
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    //取消當前進行的操作組合
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    //用imageURLKey做key,將url作為屬性繫結到UIView上
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //“&”在這裡是個位運算,if括號裡面的運算表示options的值不是“延遲佔點陣圖展示”的話就成立。
    if (!(options & SDWebImageDelayPlaceholder)) {
        //巨集定義,回到主執行緒中設定圖片(設定佔點陣圖)
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        //簡單翻譯就是 設定了activityView(正在載入指示器)為顯示的話,那麼就呼叫顯示activityView的方法...
        if ([self sd_showActivityIndicatorView]) {
            //去建立菊花(activityView)然後轉起來...
            [self sd_addActivityIndicator];
        }
        
        __weak __typeof(self)wself = self;
        //下載圖片的方法
        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];
            //如果 self被提前釋放了,直接返回
            if (!sself) { return; }
            //建立個bool變數 shouldCallCompletedBlock 只要圖片下載完成或者options設定為不需要自動設定圖片,它的值也為真。
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            //同樣 如果有圖片但是options值為SDWebImageAvoidAutoSetImage,或者沒有返回圖片且options值為SDWebImageDelayPlaceholder 值為真。
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            
            //一個block(執行完成回撥)
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                
                //如果 self被提前釋放了,直接返回
                if (!sself) { return; }
                //需要直接設定圖片的話,直接呼叫setNeedsLayout
                if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                }
                //將圖片用block回撥回去。
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, error, cacheType, url);
                }
            };
            
            // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
            // OR
            // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
            
            if (shouldNotSetImage) {
                //主執行緒中執行 callCompletedBlockClojure block
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            //如果我們獲取到了圖片,而且沒有設定“禁止自動設定圖片”,則將圖片賦值給targetImage,將圖片資料賦值給targetData.
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                targetImage = image;
                targetData = data;
            }
            //如果我們沒有獲取到圖片,而且我們設定了“延遲設定佔點陣圖”,則將佔點陣圖賦值給targetImage,targetData設定為nil.
            else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                targetImage = placeholder;
                targetData = nil;
            }
            
            BOOL shouldUseGlobalQueue = NO;
            //判斷是否設定圖片需要在併發對列中執行
            //context是一個字典,可以設定key為“SDWebImageInternalSetImageInGlobalQueueKey”的值
            if (context && [context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey]) {
                shouldUseGlobalQueue = [[context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey] boolValue];
            }
            //三目運算,如果需要在全域性併發佇列執行則獲取獲取全域性併發佇列賦值給targetQueue,否則賦值主執行緒。
            dispatch_queue_t targetQueue = shouldUseGlobalQueue ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) : dispatch_get_main_queue();
            
            dispatch_async(targetQueue, ^{
                //在目標佇列中設定圖片
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                //主執行緒回撥
                dispatch_main_async_safe(callCompletedBlockClojure);
            });
        }];
        //為UIView繫結新的操作,因為之前把UIView上的操作取消了。
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
        
    } else {
        //如果url為空的話,
        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);
            }
        });
    }
}
複製程式碼

這個方法裡面程式碼有點多,剛開始一般過一遍都比較模稜兩可。不過跟著註釋應該能瞭解個大概的流程了。其實,總的來說這個方法差不多做了這三件事:

  1. 根據key取消當前的操作組合
  2. url作為屬性繫結到UIView
  3. SDWebImageManager根據url下載圖片

我們一個一個來看看:

1.取消當前操作
//根據key取消當前進行的操作組合
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
複製程式碼

這個很有必要,舉個栗子:當tableViewcell包含了UIImageView被重用的時候,首先會執行這個方法,保證了下載和快取操作組合被取消了。 當我們點進方法去看可以看到它是在UIView+WebCacheOperation裡實現的:

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    //遍歷UIView上的所有正在下載的佇列,並取消佇列
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    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];
        }
        //取消佇列之後,移除字典中key對應的Value,也就是所有的operations
        [operationDictionary removeObjectForKey:key];
    }
}
複製程式碼
  • 在這個方法裡面有一個SDOperationsDictionary型別的operationDictionary字典,其實SDOperationsDictionaryNSMutableDictionary的一個型別別名,它這樣定義的:
typedef NSMutableDictionary<NSString *, id> SDOperationsDictionary;
複製程式碼

這裡[self operationDictionary]返回了一個字典賦給了operationDictionary

  • 看看operationDictionary方法的實現:
- (SDOperationsDictionary *)operationDictionary {
    
    //SDOperationsDictionary是NSMutableDictionary的一個型別別名,跟之前版本有點不一樣
    //這裡用到了runtime的型別繫結,給UIView繫結了一個SDOperationsDictionary的operations
    //先通過key來獲取UIView的operations屬性值
    SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    //如果能獲取到值,直接返回
    if (operations) {
        return operations;
    }
    //如果沒有值,則初始化一個空的字典繫結給UIView,然後返回
    operations = [NSMutableDictionary dictionary];
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}
複製程式碼

註釋還算清楚,用runtimeUIView繫結了一個operation字典,字典的key是針對針對不同型別的試圖和不同型別的操作設定的字串,字典的value是具體的操作(operation)。

2.將url作為屬性繫結到UIView
//用imageURLKey做key,將url作為屬性繫結到UIView上
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
複製程式碼

這裡也是利用了下runtime的型別繫結,用imageURLKey做為key,將url繫結到UIView上。這樣,我們可以通過當前的UIView直接獲取圖片的url

3.根據url下載圖片
//下載圖片的方法
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
}
複製程式碼

我們可以看到,它是由一個SDWebImageManager單例來呼叫loadImageWithURL方法,最後會返回一個SDWebImageOperation類型別的operation

1.我們點進去看看這個SDWebImageManager是怎麼初始化的:

//單例
+ (nonnull instancetype)sharedManager {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

//初始化,建立一個SDImageCache單例和SDWebImageDownloader單例,
- (nonnull instancetype)init {
    SDImageCache *cache = [SDImageCache sharedImageCache];
    SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
    return [self initWithCache:cache downloader:downloader];
}

- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        //賦值SDImageCache和SDWebImageDownloader
        _imageCache = cache;
        _imageDownloader = downloader;
        //新建一個NSMutableSet來儲存下載失敗的url.
        _failedURLs = [NSMutableSet new];
        //新建一個NSMutableArray儲存下載中的Operation
        _runningOperations = [NSMutableArray new];
    }
    return self;
}
複製程式碼

在初始化中,我們還初始化了四個了變數:

  • SDImageCache型別的_imageCache,實現快取管理的變數;
  • SDWebImageDownloader型別的_imageDownloader,實現圖片下載的變數;
  • NSMutableSet型別的_failedURLs,存那些失敗的url的變數;
  • NSMutableArray型別的_runningOperations,存放正在執行的任務的比變數。

這裡要囉嗦下的就是為什麼_failedURLs要用NSMutableSet型別而不用NSMutableArray型別。因為我們在搜尋列表中元素的時候,NSSet要比NSArray更高效,NSSet裡面的實現用到hash(雜湊)演算法,存和取都可以快速的定位。而在NSSArray中,要找其中某個元素,基本上都是要把所有元素遍歷一遍,很明顯的效率就低了很多。還有就是NSSet裡面不會有重複的元素,同一個下載失敗的url也只會存在一個。(看吧,小技巧~)

2.看它的下載方法裡面是怎麼實現的

//通過url建立一個Operation
- (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");

    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    //如果傳入的url是字串型別,則將url轉換成NSURL型別(容錯處理)
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    //如果經過上述處理後的url還是不是NSURL型別,那麼將url設定為nil.
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    if (url) {
        //建立一個互斥鎖防止,保證執行緒安全
        @synchronized (self.failedURLs) {
            //判斷url是否是下載失敗過,
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }
    //如果url不存在那麼直接返回一個block,如果url存在那麼繼續
    //如果options的值沒有設定為失敗後重試並且url下載失敗過,執行完成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;
    }
    //將operations加入到runningOperations陣列中
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    //根據url獲取到對應的key
    NSString *key = [self cacheKeyForURL:url];
    //根據key非同步在快取中查詢是否有圖片,且返回一個operation
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        //如果operation取消了,將它從self.runningOperations中移除
        if (operation.isCancelled) {
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
        //條件1.在快取中沒有找到圖片或者options設定為了SDWebImageRefreshCached
        //條件2.代理允許下載,或者代理不響應imageManager:shouldDownloadImageForURL:方法
        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.
                //傳遞快取中的image,同時嘗試重新下載它來讓NSURLCache有機會接收伺服器端的更新
                [self callCompletionBlockForOperation:weakOperation 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
            //設定downloaderOptions
            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) {
                // 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;
            }
            //到這裡才真的去下載圖片...
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                //如果操作取消,do nothing
                if (!strongOperation || strongOperation.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
                }
                //如果有錯誤,將error回撥出去
                else if (error) {
                    
                    [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
                        && error.code != NSURLErrorNetworkConnectionLost) {
                        @synchronized (self.failedURLs) {
                            //將url新增到失敗列表中、
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    //如果設定了下載失敗重試,則將url從列表中移除、
                    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), ^{
                            //呼叫代理方法完成圖片的transform
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
                            //對已經transform的圖片進行快取
                            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];
                            }
                            //執行回撥
                            [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];
                    }
                }
                //如果已經完成下載了,將operation移除
                if (finished) {
                    [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];
                };
            }
            
        }
        //其他情況:代理不允許下載,或者是沒有設定更新快取
        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];
            //移除operation
            [self safelyRemoveOperationFromRunning: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];
            //移除operation
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;
}
複製程式碼

看上去程式碼量有點多,邏輯很複雜,其實確實也是這樣的~(哈哈,我剛看的時候,腦殼都要繞暈了~)。不過整理下發現,其實差不多也就是做了兩件事:

  1. 通過url對應的key去快取裡找有沒有對應的圖片;
  2. self.imageDownloadem呼叫下載方法去下載圖片。

然後要補充的幾點是:

  • SDWebImageCombinedOperation
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

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

@end

@implementation SDWebImageCombinedOperation

- (void)setCancelBlock:(nullable SDWebImageNoParamsBlock)cancelBlock {
    // check if the operation is already cancelled, then we just call the cancelBlock
    if (self.isCancelled) {
        if (cancelBlock) {
            cancelBlock();
        }
        _cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes
    } else {
        _cancelBlock = [cancelBlock copy];
    }
}

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

@end
複製程式碼

它有三個屬性,一個是否取消的Bool屬值,一個取消``Bool和一個快取operation,很明顯,我們可以通過它來取消下載operation`。

在上面的下載方法中,有一個__block修飾的operation和一個__weak修飾的weakOperation

__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
複製程式碼

我們說下***__block*** 和 ***__weak***的區別: __block用於指明當前宣告的變數被block捕獲後,可以在block中改變變數的值。因為在block宣告的同時會捕獲該block所使用的全部自動變數的值,這些值在block中只有“使用權”而沒有“修改權”。而用__block修飾的變數,則可以在block改變變數的值了。同時,__block並不是弱引用,如果有迴圈引用的話,我們需要在block內部手動釋放,將其設定為nil__weak是所有權修飾符,__weak本身是可以避免迴圈引用的,我們在block內部使用的時候,需要用__strong來強引用,防止變數提前釋放掉了。

4.在快取中查詢圖片:queryCacheOperationForKey

這個方法是SDImageCache裡面定義的一個方法,我們開方法的實現:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    //如果key為空,回撥完成block,return
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // First check the in-memory cache...
    //首先通過key在記憶體的快取中查詢有沒有圖片...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    //如果有記憶體快取中有圖片
    if (image) {
        NSData *diskData = nil;
        //如果gif圖片(非動圖)
        if (image.images) {
            //通過key找到磁碟中圖片的data
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        //執行完成block
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }

    //如果記憶體中沒有,則在磁碟中查詢,如果找到,則將其放在記憶體快取中,並呼叫doneBlock
    NSOperation *operation = [NSOperation new];
    //在ioQueue中序列處理所有磁碟快取
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        //建立自動釋放池,記憶體及時釋放
        @autoreleasepool {
            //根據key去磁碟中找到圖片的data資料
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            //根據圖片的url的對應的key去磁碟快取中查詢圖片
            UIImage *diskImage = [self diskImageForKey:key];
            //如果磁碟中有圖片,並且設定了記憶體快取機制,再將圖片快取在記憶體中
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                //用NSCache將圖片儲存到記憶體中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            if (doneBlock) {
                //主執行緒執行完成block
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}
複製程式碼

方法中用到了一個序列佇列self.ioQueue,它的宣告和初始化是這樣的:

@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;
// Create IO serial queue
ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
複製程式碼

總結

到目前為止,只進行到了通過key去快取中查詢有沒有圖片,在完成回撥中,我們經過一些邏輯處理,到最後面才去用self.imageDownloader去呼叫download方法。下篇我們可能就會去了解下download方法是如何下載的,網路是怎麼處理的。

(PS,有很多理解不到位的地方,還希望和大家一起交流學習~)

相關文章