原始碼閱讀:SDWebImage(一)——從使用入手

堯少羽發表於2018-05-22

該文章閱讀的SDWebImage的版本為4.3.3。

1.日常使用

在日常的使用中,通常是載入網路圖片到UIImageView上展示,所以一般在需要使用SDWebImage的檔案中只引用#import "UIImageView+WebCache.h"標頭檔案。

最簡單的載入方式是隻載入圖片地址:

UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:imageView];
    
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]];
複製程式碼

當然,SDWebImage也提供了其他的載入方法,不過點選方法進入檢視後,發現最終都是呼叫其全能方法:

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

全能方法除了必需的的圖片地址,還提供了佔點陣圖、可選項、載入進度和完成回撥。

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
複製程式碼

緊接著我們點選進入全能方法中:

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:nil
                            progress:progressBlock
                           completed:completedBlock];
}
複製程式碼

可以發現,全能方法並沒有什麼實際的實現,只是對另一個方法的封裝。

2.再進一步

在上一段中我們進入了UIImageView+WebCache分類中檢視圖片載入的實現,發現其實是呼叫了另外一個方法,而這個方法在另一個分類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;
複製程式碼

接著看這個方法的實現:

- (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 {
    return [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey setImageBlock:setImageBlock progress:progressBlock completed:completedBlock context:nil];
}
複製程式碼

發現這個方法也是呼叫了一個全能方法:

- (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<NSString *, id> *)context;
複製程式碼

再進入到這個方法中檢視,發現這個方法就是載入圖片的核心實現

3.核心實現

通過不斷的點選進入方法檢視,我們從UIImageView+WebCache分類跳到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
                           context:(nullable NSDictionary<NSString *, id> *)context {
                           
    // 生成一個有效的操作金鑰,如果傳入了引數就用傳入的,否則就用當前類的類名
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    // 取消該金鑰對應的圖片載入操作
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    
    // 通過關聯物件,將傳入的圖片地址儲存到靜態變數imageURLKey中
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // 如果沒有選擇延遲載入佔點陣圖
    if (!(options & SDWebImageDelayPlaceholder)) {
        // 如果context中有排程組,就獲取並進入排程組
        if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
            dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
            dispatch_group_enter(group);
        }
        // 在主執行緒主佇列中設定佔點陣圖
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    // 如果傳入了圖片連結
    if (url) {
        // 如果設定要顯示載入小菊花,就新增載入小菊花並開始動畫
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        // 初始化圖片載入進度
        self.sd_imageProgress.totalUnitCount = 0;
        self.sd_imageProgress.completedUnitCount = 0;
        
        // 生成圖片管理者,如果context中有就用context中的,否則就直接生成
        SDWebImageManager *manager;
        if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
            manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
        } else {
            manager = [SDWebImageManager sharedManager];
        }
        
        // 生成一個程式碼塊用來在下載圖片的方法中監聽進度並進行回撥
        __weak __typeof(self)wself = self;
        SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            wself.sd_imageProgress.totalUnitCount = expectedSize;
            wself.sd_imageProgress.completedUnitCount = receivedSize;
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        
        // 生成圖片操作物件,並開始下載圖片
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        
            // 如果沒有生成強引用的self就終止執行
            __strong __typeof (wself) sself = wself;
            if (!sself) { return; }
            
            // 如果有載入小菊花的話就移除掉
            [sself sd_removeActivityIndicator];
            
            // 如果已經完成並且沒有錯誤,並且進度沒有更新,就將進度狀態設為未知
            if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
                sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
            // 是否應該回撥完成blok: 如果已經完成或者設定了在設定圖片前處理
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            // 是否應該不設定圖片: 如果有圖片但設定了在設定圖片前處理,或者沒有圖片並且沒有設定延遲載入佔點陣圖
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
                                      
            // 生成完成回撥程式碼塊
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                // 如果沒有生成強引用的self就終止執行 
                if (!sself) { return; }
                // 如果需要設定圖片就直接重新整理檢視
                if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                }
                // 如果傳入了回撥block並且應該進行回撥,就直接回撥
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, error, cacheType, url);
                }
            };
            
            // 如果不需要設定圖片就在主執行緒主佇列中呼叫上面生成的完成回撥程式碼塊,並且不再向下執行
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            
            // 生成變數儲存資料
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            
            if (image) {
                // 如果圖片下載成功就用變數儲存圖片
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // 如果圖片下載失敗並且設定了延遲載入佔點陣圖,就儲存佔點陣圖
                targetImage = placeholder;
                targetData = nil;
            }
            
            // 檢查一下是否應該轉換圖片:如果下載完成,並且設定了圖片強制轉換或者圖片快取型別是不快取直接從網路載入,就進行強制轉換
            SDWebImageTransition *transition = nil;
            if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
                transition = sself.sd_imageTransition;
            }
            
            // 如果context中有排程組,就獲取並進入排程組
            if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
                dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
                dispatch_group_enter(group);
                
                // 在主執行緒主佇列中設定圖片
                dispatch_main_async_safe(^{
                    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
                });
                
                // 排程完成後呼叫完成回撥程式碼塊
                dispatch_group_notify(group, dispatch_get_main_queue(), ^{
                    callCompletedBlockClojure();
                });
            } else {
            
                // 如果使用者沒有設定排程組,就直接在主執行緒主佇列中設定圖片和呼叫完成回撥程式碼塊
                dispatch_main_async_safe(^{
                    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
                    callCompletedBlockClojure();
                });
            }
        }];
        
        // 根據金鑰儲存下載圖片的操作
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
        
    } else {
    
        // 如果沒傳入圖片連結,就在主執行緒主佇列移除載入小菊花
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            
            // 如果傳入了完成回撥block就回撥錯誤資訊
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}
複製程式碼

這就是載入圖片核心邏輯,接下來我們會看一下一些重要的方法

4.如何防止串圖?

在使用SDWebImage的時候,大多數時候都是載入網路圖片,為了保證UI的流暢性,通常是將載入過程放到子執行緒中。由於是非同步載入,所以如果在同一個控制元件先後載入不同的圖,比如使用複用機制的UITableViewCell,就可能出現串圖的問題。SDWebImage是怎麼解決的呢?我們來看一下。

在上面的方法實現中,我們可以在開始和結束的位置看到這樣一對方法:

[self sd_cancelImageLoadOperationWithKey:validOperationKey];
複製程式碼
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
複製程式碼

分別點選進入檢視實現:

/**
 這個方法是取消掉該控制元件上validOperationKey對應的圖片載入操作
 */
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {

    // 獲取繼承自NSMapTable的自定義字典物件
    SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
    
    // 在加鎖保護下獲取到操作物件
    id<SDWebImageOperation> operation;
    @synchronized (self) {
        operation = [operationDictionary objectForKey:key];
    }
    
    // 如果獲取到了操作物件就取消,並且從字典中移除
    if (operation) {
        if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]){
            [operation cancel];
        }
        @synchronized (self) {
            [operationDictionary removeObjectForKey:key];
        }
    }
}
複製程式碼
/**
 這個方法是將該控制元件上的圖片載入操作和validOperationKey一一對應儲存起來
 */
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
    // 必須要傳入引數key
    if (key) {
        // 先取消掉key對應的操作
        [self sd_cancelImageLoadOperationWithKey:key];
        // 必須要傳入引數operation
        if (operation) {
            // 獲取到儲存用的字典,並線上程安全的狀態下儲存
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self) {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}
複製程式碼

可以看到,在載入圖片的一開始會先將該控制元件上validOperationKey對應的操作取消掉,然後在生成新的操作時將操作與validOperationKey一一對應儲存起來,這樣就能保證了在載入新圖片時,一個控制元件上只有一個圖片載入操作正在進行。

5.安全的主佇列非同步載入

在設定圖片的時候,無論是載入好的圖片還是佔點陣圖,都是在一個自定義的巨集中進行的:

dispatch_main_async_safe(^{
            // do something
        });
複製程式碼

我們點選這個巨集進入檢視:

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
#endif
複製程式碼

可以看到這個巨集是對另一個巨集dispatch_queue_async_safe(dispatch_get_main_queue(), block)的封裝,其中一個引數傳入了dispatch_get_main_queue()主佇列,繼續點選進入檢視:

#ifndef dispatch_queue_async_safe
#define dispatch_queue_async_safe(queue, block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(queue)) == 0) {\
        block();\
    } else {\
        dispatch_async(queue, block);\
    }
複製程式碼
  • 其中int strcmp(const char *__s1, const char *__s2);這個函式是用來比較兩個字串是否相等,如果返回0就說明兩個傳入的字串相等。
  • 還有const char * dispatch_queue_get_label(dispatch_queue_t queue);這個函式是獲取傳入的佇列的名字,如果傳入了引數DISPATCH_CURRENT_QUEUE_LABEL,就是獲取當前所在佇列的名字。

所以這個巨集的意思就是:如果當前所在佇列是主佇列就直接執行程式碼塊;如果當前所在佇列不是主佇列就在主佇列非同步執行傳入的程式碼塊,其實就是確保傳入的程式碼塊block在主執行緒主佇列中呼叫。

6.設定圖片

無論是設定下載好的圖還是佔點陣圖,都是呼叫了同一個方法:

- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock {
    [self sd_setImage:image imageData:imageData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:nil cacheType:0 imageURL:nil];
}
複製程式碼

這個方法就是將圖片設定到控制元件上的主方法

- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
    // 獲取到當前檢視
    UIView *view = self;
    // 設定臨時變數儲存設定圖片程式碼塊
    SDSetImageBlock finalSetImageBlock;
    // 如果設定了設定圖片程式碼塊就直接設定程式碼塊
    if (setImageBlock) {
        finalSetImageBlock = setImageBlock;
    }
#if SD_UIKIT || SD_MAC
    // 如果是該類是圖片檢視,就建立設定圖片程式碼塊並在其中為圖片檢視設定圖片
    else if ([view isKindOfClass:[UIImageView class]]) {
        UIImageView *imageView = (UIImageView *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData) {
            imageView.image = setImage;
        };
    }
#endif
#if SD_UIKIT
    // 如果是該類是按鈕,就建立設定圖片程式碼塊並在其中為按鈕在正常狀態下設定圖片
    else if ([view isKindOfClass:[UIButton class]]) {
        UIButton *button = (UIButton *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData){
            [button setImage:setImage forState:UIControlStateNormal];
        };
    }
#endif
    
    // 如果設定了過度
    if (transition) {
#if SD_UIKIT
        [UIView transitionWithView:view duration:0 options:0 animations:^{
            // 0 duration to let UIKit render placeholder and prepares block
            // 如果在展示過渡動畫之前設定了要執行的程式碼塊就先執行
            if (transition.prepares) {
                transition.prepares(view, image, imageData, cacheType, imageURL);
            }
        } completion:^(BOOL finished) {
            // 開始執行動畫
            [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
                // 如果設定了程式碼塊並且沒設定避免自動設定圖片,就直接傳參並呼叫程式碼塊
                if (finalSetImageBlock && !transition.avoidAutoSetImage) {
                    finalSetImageBlock(image, imageData);
                }
                // 如果設定了動畫程式碼塊,就傳參並呼叫程式碼塊
                if (transition.animations) {
                    transition.animations(view, image);
                }
            // 如果設定了完成程式碼塊,就傳參並呼叫程式碼塊
            } completion:transition.completion];
        }];
// 這是Mac相關
#elif SD_MAC
        [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) {
            // 0 duration to let AppKit render placeholder and prepares block
            prepareContext.duration = 0;
            if (transition.prepares) {
                transition.prepares(view, image, imageData, cacheType, imageURL);
            }
        } completionHandler:^{
            [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
                context.duration = transition.duration;
                context.timingFunction = transition.timingFunction;
                context.allowsImplicitAnimation = (transition.animationOptions & SDWebImageAnimationOptionAllowsImplicitAnimation);
                if (finalSetImageBlock && !transition.avoidAutoSetImage) {
                    finalSetImageBlock(image, imageData);
                }
                if (transition.animations) {
                    transition.animations(view, image);
                }
            } completionHandler:^{
                if (transition.completion) {
                    transition.completion(YES);
                }
            }];
        }];
#endif
    } else {
        // 如果設定了設定圖片程式碼塊就傳參並呼叫程式碼塊
        if (finalSetImageBlock) {
            finalSetImageBlock(image, imageData);
        }
    }
}
複製程式碼

總結一下設定圖片的邏輯:

  • 在方法的開始,根據控制元件所屬類的不同,生成最終設定圖片的程式碼塊。
  • 接下來如果設定了過度動畫,就執行過度動畫
  • 如果沒設定過度動畫,就直接傳參呼叫在方法開始生成的最終設定圖片的程式碼塊。

7.圖片載入操作

接著我們來看最核心的方法,圖片的載入:

- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;
複製程式碼

這個方法除了必傳的圖片地址連結外,還提供了可選項、進度監聽和完成回撥。

點選進入檢視方法實現,發現這個方法的實現有點多:

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 完成回撥程式碼塊是必傳的
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // 雖然引數要求傳NSURL物件但是傳NSStriing物件不會有警告,所以做一下處理
    if ([url isKindOfClass:NSString.class]) {
        // 如果傳入的引數url是一個NSString型別物件,就轉化成NSURL型別的
        url = [NSURL URLWithString:(NSString *)url];
    }

    // 防止引數url的型別錯誤導致崩潰,如url的值是NSNull型別
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    // 生成一個圖片載入操作的封裝物件
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;

    // 請求載入圖片的地址連線是否在載入失敗的地址連結集合中,也就是說這次請求載入的圖片,以前是否載入失敗過
    BOOL isFailedUrl = NO;
    if (url) {
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }

    // 如果連結地址不正確,或者之前載入失敗過但是也沒設定失敗可重試,就直接回撥錯誤並返回
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }

    // 將當前操作物件新增到集合中儲存
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    // 生成地址連結對應的Key
    NSString *key = [self cacheKeyForURL:url];
    
    SDImageCacheOptions cacheOptions = 0;
    // 如果設定了強制記憶體和硬碟同時查詢
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    // 如果設定了強制記憶體和硬碟同步查詢
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    
    // 生成查詢圖片快取操作物件,並開始非同步查詢
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    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;
        }
        
        // 檢視是否應該從網路下載該圖片,想要從網路下載圖片必須同時滿足以下條件:
        // 沒有設定只從快取載入的選項
        // 沒找到快取圖,或者設定了需要重新整理快取圖
        // 代理物件沒有實現這個代理方法,或者代理物件實現了這個代理方法並且代理方法返回了YES,意思是在快取中找不到圖片時要從網路上下載
        BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
            && (!cachedImage || options & SDWebImageRefreshCached)
            && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
            
        // 如果需要下載圖片
        if (shouldDownload) {
            // 如果有圖片快取並且設定了重新整理圖片快取,就先進行完成回撥
            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];
            }

            // 獲取設定的操作選項
            SDWebImageDownloaderOptions downloaderOptions = 0;
            // 低優先順序
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            // 漸進顯示
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            // 重新整理快取
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            // 後臺下載
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            // 處理Cookies
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            // 允許不可信的SSL證書
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            // 高優先順序
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            // 縮小尺寸
            if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
            
            // 如果有快取圖,並且選擇了重新整理快取的選項
            if (cachedImage && options & SDWebImageRefreshCached) {
                // 關閉漸進顯示
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // 忽視從NSURLCache中獲取快取
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            
            // 開啟下載任務並獲取下載任務的令牌
            __weak typeof(strongOperation) weakSubOperation = strongOperation;
            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) {
                    // 如果沒有操作物件,或者操作物件被取消了就什麼也不做
                } else if (error) {
                    // 如果出錯,就進行回撥
                    [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
                    
                    // 是否應該阻止下載已經失敗的圖片地址連結
                    BOOL shouldBlockFailedURL;
                    
                    if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
                        // 如果實現了代理就按照代理返回的資料
                        shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
                    } else {
                        // 如果沒有實現代理,想要阻止下載失敗的連結就得滿足以下條件:
                           不是沒聯網、
                           不是被取消、
                           不是連線超時、
                           不是關閉了國際漫遊、
                           不是不允許蜂窩資料連線、
                           不是沒有找到host、
                           不是無法連線host、
                           不是連線丟失
                        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) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    // 如果下載成功
                    
                    // 如果設定了嘗試下載失敗的連結選項,就把連線從黑名單中移除
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    // 是否要快取到硬碟:如果沒設定只快取到記憶體的選項就需要快取到硬碟
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    
                    // 在單例管理物件SDWebImageDownloader中已經實現了圖片的縮放,這裡是用於自定義管理物件以避免額外的縮放
                    if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
                        // 如果當前物件不是SDWebImageManager的單例物件,並且設定裡過濾連結程式碼塊,並且下載到了圖片。就進行縮放
                        downloadedImage = [self scaledImageForKey:key image:downloadedImage];
                    }

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // 如果設定了重新整理快取選項,並且有快取圖,並且沒有下載圖,就什麼也不需要做
                    } 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];
                                NSData *cacheData;
                                
                                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), ^{
                                    NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
                                    [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                });
                            } else {
                                // 否者就直接快取圖片
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                        }
                        // 進行回撥
                        [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) {
            // 如果不需要下載並且獲取到了快取圖就進行回撥然後從操作物件集合中移除
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        } else {
            // 如果不需要下載並且也沒有快取圖就進行回撥然後從操作物件集合中移除
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        }
    }];

    // 返回操作物件
    return operation;
}
複製程式碼

看完了這個方法的實現,我們來大概的梳理一下其邏輯:

  • 首先會檢視要載入的圖片連結是否失敗過,再根據設定的選項決定要不要繼續載入。
  • 然後根據圖片連結查詢是否有該圖片的快取,再根據設定的選項決定要不要網路下載。
  • 接著根據下載的圖片是否是動圖,再根據設定的選項決定怎麼轉換動圖。
  • 最後根據設定的選項進行快取,並進行回撥。

主要的邏輯就是這樣,當然裡面還有一些具體的細節。

8.獲取圖片快取

在圖片載入的方法實現中,可以看到有比較重要的兩個方法,一個是獲取圖片快取,另一個是從網路下載圖片。在這一節,我們先看獲取圖片快取:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock;
複製程式碼

點選方法進入檢視其實現:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    // 如果沒傳引數key就直接回撥並返回,就不繼續向下執行了
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // 先根據key查詢記憶體中是否有快取
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    // 如果有快取圖片,並且沒設定強制從硬碟中查詢快取,就直接回撥並返回了
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            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 {
            // 根據key查詢硬碟中是否有快取
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeDisk;
            if (image) {
                // 如果記憶體中有快取
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                // 如果記憶體中沒有快取但是硬碟中有快取
                diskImage = [self diskImageForKey:key data:diskData];
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [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;
}
複製程式碼

獲取圖片快取的邏輯還是很清晰的:

  • 首先查詢在記憶體中的快取,再根據設定的選項決定要不要繼續查詢。
  • 然後根據設定的選項決定是同步還是非同步查詢硬碟中的快取。
  • 接著根據設定的選項決定要不要把硬碟中的快取圖片快取到記憶體中。
  • 最後進行回撥資料

9.從網路中下載圖片

這一節來看一下另一個比較重要的方法,即從網路中下載圖片:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
複製程式碼

點選方法檢視實現,發現這個方法只不過是對另一個方法的封裝:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    // 這裡直接呼叫了另一個方法,但是我們要看一下傳入的createCallback中的內容
    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        
        // 設定下載超時時長,預設為15秒
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // 根據設定的選項設定請求快取策略
        NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
        // 生成請求物件,並設定引數
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
        }
        else {
            request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
        }
        
        // 生成下載操作的封裝物件,並設定引數
        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;
        }
        
        // 如果選項設定了後進先出(LIFO),就讓上一個操作依賴於當前操作
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation is dependency
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}
複製程式碼

在傳入下一個方法的引數createCallback中,主要是生成了一個請求物件request,然後把這個請求物件放到操作物件operation中。如果設定了後進先出(LIFO),還會設定操作依賴。

看完了傳入的引數,我們來看看方法中具體做了些什麼:

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
    // 引數url依舊是必傳的
    if (url == nil) {
        // 如果未傳的話直接回撥並返回
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    
    // 加鎖,其實是利用訊號量來實現的序列
    LOCK(self.operationsLock);
    // 從載入操作物件集合中獲取url對應的操作物件
    SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
    // 如果沒有獲取到url對應的操作物件
    if (!operation) {
        // 從傳入的程式碼塊中或操作物件
        operation = createCallback();
        // 設定操作物件的完成回撥,在回撥中把url對應的操作物件從操作物件集合中移除
        __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);
        };
        // 將生成的操作物件以url為key新增到操作物件集合中,
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple is doc.
        // 將生成的操作物件新增到操作佇列中執行
        [self.downloadQueue addOperation:operation];
    }
    // 解鎖
    UNLOCK(self.operationsLock);

    // 生成一個token,方便對該操作物件進行操作
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}
複製程式碼

看完這兩個方法(或者說是一個),我們發現並沒有進行網路請求。這個方法主要做的內容是:

  • 先根據傳入的url生成NSMutableURLRequest物件request
  • 再根據上一步生成的物件request生成一個自定義的繼承自NSOperation類的SDWebImageDownloaderOperation物件operation
  • 接著把物件operation儲存到操作物件集合中,並新增到操作佇列開始執行。
  • 最後根據引數生成一個SDWebImageDownloadToken物件token返回,以便利用這個token對操作物件做一些操作。

10.真·網路請求

上一節雖然叫“從網路中下載圖片”,但是我們從程式碼中並沒有發現發起網路請求的操作,這一節,就來看看下載圖片的網路請求到底在哪兒,是怎麼實現的。

上一節中我們看到已經生成了一個網路求情物件request,按理來說,想要發起網路請求,只需利用request生成一個NSURLSessionTask物件,然後resume一下就可以。但是物件request並沒有生成NSURLSessionTask物件,而是生成了一個作者自定義的類物件——繼承自NSOperation類的SDWebImageDownloaderOperation類物件operation。然後直接新增到操作佇列執行了。所以這個SDWebImageDownloaderOperation類中一定存在一個大陰謀!

在一個NSOperation類物件開始執行的時候會呼叫其物件方法- (void)start;,所以我們在自定義類SDWebImageDownloaderOperation中,看到作者重寫了這個方法:

- (void)start {
    // 加鎖
    @synchronized (self) {
        // 如果已經被設定為取消狀態就直接復位,然後返回了
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if SD_UIKIT
        // 檢視是否能獲取到UIApplication類
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        // 如果有UIApplication類,並且設定了進入後臺依舊下載的選項
        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;
        // 如果沒有傳入網路會話物件
        if (!session) {
            // 生成一個網路會話物件
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        
        // 如果設定了忽略從NSURLCache中獲取快取的選項
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // 獲取NSURLCache中的快取,並儲存
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        
        // 生成NSURLSessionTask類物件
        self.dataTask = [session dataTaskWithRequest:self.request];
        // 設定屬性為開始執行
        self.executing = YES;
    }

    // 如果NSURLSessionTask類物件存在
    if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
        // 因為NSURLSessionTask的priority這個屬性是iOS8.0以後才有的,所以要判斷一下
        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 {
        // 如果沒有獲取到NSURLSessionTask類物件,就回撥錯誤並返回
        [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
}
複製程式碼

這就是SDWebImage在從網路載入圖片的網路請求方法了,邏輯也是清晰明瞭:

  • 先判斷這個操作物件是否已經被取消了,被取消就不繼續執行了。
  • 再判斷是否設定了後臺下載選項,進行後臺下載設定。
  • 接著設定網路會話物件。
  • 再根據設定的選項判斷是否忽略NSURLCache的快取。
  • 接著根據設定的選項設定任務物件的優先順序。
  • 然後啟動任務,呼叫進度回撥程式碼塊,傳送通知。
  • 最後如果設定了後臺下載選項,就結束後臺下載任務。

11.網路請求代理回撥

上面所有的實現都是為了發起網路請求,發起請求是一方面,處理請求結果又是另一個方面,這一節主要看的是SDWebImage對網路請求結果的處理。對結果的處理主要集中在任務物件的代理方法中:

原始碼閱讀:SDWebImage(一)——從使用入手

11.1.NSURLSessionDataDelegate代理實現

/**
 當dataTask接收到初試響應時,會呼叫這個代理方法
 */
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    // 生成變數儲存響應處置為允許繼續
    NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
    // 生成變數儲存響應內容的預期長度,如果沒獲取到設定為0
    NSInteger expected = (NSInteger)response.expectedContentLength;
    expected = expected > 0 ? expected : 0;
    // 用屬性儲存響應內容的預期長度
    self.expectedSize = expected;
    // 用屬性儲存響應物件
    self.response = response;
    // 生成變數儲存狀態碼,如果沒獲取到設為為200
    NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
    // 生成變數儲存有效性,如果狀態碼小於400就有效
    BOOL valid = statusCode < 400;
    // 如果狀態碼是304,並且沒有快取資料,也是無效的
    if (statusCode == 304 && !self.cachedData) {
        valid = NO;
    }
    
    if (valid) {
        // 如果是有效的就呼叫進度回撥程式碼塊
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
    } else {
        // 如果是無效的就設定響應處置為取消
        disposition = NSURLSessionResponseCancel;
    }
    
    // 主佇列非同步傳送通知告知已收到響應
    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf];
    });
    
    // 呼叫完成處理程式碼塊並傳遞響應處置設定
    if (completionHandler) {
        completionHandler(disposition);
    }
}
複製程式碼
/**
 當dataTask在接收到資料時,會呼叫這個代理方法,這個方法在接受資料期間會反覆呼叫
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    // 如果儲存圖片資料的屬性為空,就以上面那個代理方法中獲取的圖片期望大小為容量,初始化這個屬性
    if (!self.imageData) {
        self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
    }
    // 將伺服器返回的資料新增到屬性中儲存
    [self.imageData appendData:data];

    // 如果設定了漸進式下載的選項,並且圖片的期望大小大於零
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
        // 獲取到當前獲取的圖片資料
        __block NSData *imageData = [self.imageData copy];
        // 獲取到當前下載的圖片的位元組數
        const NSInteger totalSize = imageData.length;
        // 獲取當前圖片是否下載完成,如果當前下載的圖片的位元組數不少於圖片期望的位元組數就為完成
        BOOL finished = (totalSize >= self.expectedSize);
        
        // 如果沒有例項化漸進式編碼器
        if (!self.progressiveCoder) {
            // 遍歷所有的編碼器,如果編碼器實現了SDWebImageProgressiveCoder代理,並且能夠解碼下載的圖片資料,就以該編碼器的類例項化一個編碼器物件
            for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
                if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
                    [((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
                    self.progressiveCoder = [[[coder class] alloc] init];
                    break;
                }
            }
        }
        
        // 自定義序列佇列非同步執行
        dispatch_async(self.coderQueue, ^{
            // 解碼生成圖片
            UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
            // 如果生成了圖片
            if (image) {
                // 獲取該圖片連結地址對應的key
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                // 縮放圖片
                image = [self scaledImageForKey:key image:image];
                // 如果壓縮圖片就壓縮
                if (self.shouldDecompressImages) {
                    image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
                }
                
                // 進行完成回撥
                [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
            }
        });
    }

    // 回撥進度
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }
}
複製程式碼
/**
 當dataTask完成接收所有預期資料後會呼叫這個代理方法,詢問代理物件是否應將響應儲存在快取中。
 */
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
    
    // 獲取到快取響應
    NSCachedURLResponse *cachedResponse = proposedResponse;

    // 如果使用者設定了進行快取就回撥快取響應,否則就回撥nil不快取
    if (!(self.options & SDWebImageDownloaderUseNSURLCache)) {
        cachedResponse = nil;
    }
    // 呼叫完成程式碼塊並傳參
    if (completionHandler) {
        completionHandler(cachedResponse);
    }
}
複製程式碼

11.2.NSURLSessionTaskDelegate代理實現

/**
 當dataTask已經完成傳輸資料時會呼叫這個代理方法
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    @synchronized(self) {
        // 將儲存任務物件的屬性置空
        self.dataTask = nil;
        // 主佇列非同步傳送下載停止通知,如果沒有錯誤就傳送下載完成通知
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
            if (!error) {
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
            }
        });
    }
    
    if (error) {
        // 如果出錯就回撥錯誤
        [self callCompletionBlocksWithError:error];
        // 呼叫完成方法
        [self done];
    } else {
        // 如果有完成回撥的程式碼塊
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            // 如果有下載的圖片資料
            __block NSData *imageData = [self.imageData copy];
            if (imageData) {
                // 如果設定了忽略快取響應選項,並且快取資料與下載的圖片資料相同
                if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
                    // 回撥nil,並呼叫完成方法
                    [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
                    [self done];
                } else {
                    // 在自定義序列佇列非同步執行
                    dispatch_async(self.coderQueue, ^{
                        // 對下載的圖片資料進行解碼
                        UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                        // 獲取圖地址連結對應的key
                        NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                        // 對圖片進行縮放
                        image = [self scaledImageForKey:key image:image];
                        
                        // 生成變數儲存圖片是否應該解碼
                        BOOL shouldDecode = YES;
                        if (image.images) {
                            // 如果是gif圖就不解碼
                            shouldDecode = NO;
                        } else {
#ifdef SD_WEBP
                            // 如果是webp格式的也不轉碼
                            SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                            if (imageFormat == SDImageFormatWebP) {
                                shouldDecode = NO;
                            }
#endif
                        }
                        
                        // 如果需要轉碼
                        if (shouldDecode) {
                            // 如果需要解壓圖片
                            if (self.shouldDecompressImages) {
                                // 獲取是否需要縮小圖片
                                BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
                                // 對圖片進行解壓
                                image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
                            }
                        }
                        // 獲取處理好的圖片尺寸
                        CGSize imageSize = image.size;
                        if (imageSize.width == 0 || imageSize.height == 0) {
                            // 如果長或寬有一側為0,就回撥錯誤
                            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                        } else {
                            // 如果長寬都有資料,就完成回撥
                            [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
                        }
                        // 呼叫完成方法
                        [self done];
                    });
                }
            } else {
                // 如果沒有下載圖片資料就回撥錯誤
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
                // 呼叫完成方法
                [self done];
            }
        } else {
            // 如果沒有完成回撥的程式碼塊就呼叫完成方法
            [self done];
        }
    }
}
複製程式碼
/**
 當task接收到身份驗證時,會呼叫這個代理方法。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    
    // 設定臨時變數儲存資料
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
    
    // 如果驗證方式為NSURLAuthenticationMethodServerTrust
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
            // 如果沒設定允許不可信的SSL證書,就設定處理方式為預設
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        } else {
            // 如果設定了允許不可信的SSL證書,就設定驗證模式為通過指定證書驗證,並生成證書
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    } else {
        如果驗證方式不是NSURLAuthenticationMethodServerTrust
        if (challenge.previousFailureCount == 0) {
            // 如果認證的失敗次數設定為0次
            if (self.credential) {
                // 如果有證書,就設定證書,驗證模式為通過指定證書驗證
                credential = self.credential;
                disposition = NSURLSessionAuthChallengeUseCredential;
            } else {
                // 如果沒有證書,就設定驗證模式為不需要驗證證書
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            // 如果認證的失敗次數設定超過0次,就設定驗證模式為不需要驗證證書
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        }
    }
    
    // 呼叫完成程式碼塊並傳參
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}
複製程式碼

看完對代理方法的實現,比較主要的邏輯就是對下載完成後的圖片資料進行處理:

  • 先解碼圖片資料生成圖片。
  • 再對圖片進行縮放。
  • 最後對圖片進行解壓。

其他的一些邏輯主要是針對選項引數設定的不同,進行了不同的處理。

12.總結

我們看完了一張圖片載入的全過程,最後我們再來總結一下:

  • 我們在使用SDWebImage時呼叫了[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]];這個簡單的分類方法,然後就靜靜的等著圖片被設定到UIImageView類物件上。
  • 經過一系列呼叫,我們首先來到UIView+WebCache分類中,在這個分類中,首先保障了圖片載入的唯一性,然後就開始了核心的載入操作。
  • 接著就進入了SDWebImageManager類中,在這個類中,首先去查詢是否有快取,沒有快取的話才去伺服器下載。
  • 想要查詢快取我們要進入SDImageCache這個類中,在這個類中,首先去記憶體中檢視是否有對應的快取,如果沒有再去硬碟中查詢是否有對應的快取,但是從硬碟中獲取的是圖片的資料,要想獲得圖片還要經歷解碼、縮放和解壓。當然如果都沒有快取的話就去下載。
  • 負責下載的是SDWebImageDownloader這個類,在這個類中,將圖片的下載操作封裝成了自定義的一個類SDWebImageDownloaderOperation,然後新增到了操作佇列中。
  • 當操作佇列呼叫這個操作時,會呼叫操作物件的- (void)start方法,在重寫的這個方法中,生成了任務物件dataTask,並呼叫resume開始執行任務。
  • 因為SDWebImageDownloaderOperation類遵守了dataTask物件的協議,所以dataTask執行的結果會通過代理方法進行回撥。在代理方法中,獲取並儲存了伺服器返回的資料,並在任務執行結束後,對資料進行解碼、縮放和解壓。處理完成後就進行回撥。
  • 通過重重回撥,要回撥的資料沿著SDWebImageDownloaderOperation->SDWebImageDownloader->SDWebImageManager->UIView+WebCache一路流動,其中流動到SDWebImageManager中時對圖片進行了快取,最後在UIView+WebCache中為UIImageView設定了處理好的圖片。
  • 這就是SDWebImage載入圖片的大體流程,當然還有非常多的小細節和小功能,這些就在以後對具體類的閱讀中學習了。

最後附上SDWebImage介面文件的兩張圖:

原始碼閱讀:SDWebImage(一)——從使用入手
這張圖展示了各類之間的關係

原始碼閱讀:SDWebImage(一)——從使用入手
這張圖展示了載入圖片的流程

原始碼閱讀系列:SDWebImage

原始碼閱讀:SDWebImage(一)——從使用入手

原始碼閱讀:SDWebImage(二)——SDWebImageCompat

原始碼閱讀:SDWebImage(三)——NSData+ImageContentType

原始碼閱讀:SDWebImage(四)——SDWebImageCoder

原始碼閱讀:SDWebImage(五)——SDWebImageFrame

原始碼閱讀:SDWebImage(六)——SDWebImageCoderHelper

原始碼閱讀:SDWebImage(七)——SDWebImageImageIOCoder

原始碼閱讀:SDWebImage(八)——SDWebImageGIFCoder

原始碼閱讀:SDWebImage(九)——SDWebImageCodersManager

原始碼閱讀:SDWebImage(十)——SDImageCacheConfig

原始碼閱讀:SDWebImage(十一)——SDImageCache

原始碼閱讀:SDWebImage(十二)——SDWebImageDownloaderOperation

原始碼閱讀:SDWebImage(十三)——SDWebImageDownloader

原始碼閱讀:SDWebImage(十四)——SDWebImageManager

原始碼閱讀:SDWebImage(十五)——SDWebImagePrefetcher

原始碼閱讀:SDWebImage(十六)——SDWebImageTransition

原始碼閱讀:SDWebImage(十七)——UIView+WebCacheOperation

原始碼閱讀:SDWebImage(十八)——UIView+WebCache

原始碼閱讀:SDWebImage(十九)——UIImage+ForceDecode/UIImage+GIF/UIImage+MultiFormat

原始碼閱讀:SDWebImage(二十)——UIButton+WebCache

原始碼閱讀:SDWebImage(二十一)——UIImageView+WebCache/UIImageView+HighlightedWebCache

相關文章