以不一樣的方式理解SDWebImage

本文由 iMetalk 團隊的成員 Lefe 完成,主要幫助讀者深入理解一個第三方庫

本文不會教你咋么使用SD,而是要告訴你如何讀懂SD,掌握SD的原理及架構。可能,你也看過別人的對SD的源碼解析,不過 Lefe 上網(wǎng)看了一下,大部分都是以一種簡單的方式介紹SD。本文主要通過不同的角度來學習SD,主要從以下方面著手:

  • 各個文件的作用是什么
  • SD 使用的知識點總結
  • SD 中的思想
  • 時序圖
  • SD類圖
  • 使用實例
  • 總結

各個文件的作用是什么

擴展文件( UIView + ... ):

這些文件讓使用者更簡單的使用,基本是傻瓜式的,你可以在不懂 SD 的情況下寫出高性能的圖片加載。這就是 SD 的優(yōu)點所在。

  • UIView+WebCache.h

這個文件可以說是其它視圖加載圖片的關鍵,其它擴展是基于 UIView 擴展的基礎上,實現(xiàn)了視圖本身加載圖片的方式。它和 UIView+WebCacheOperation.h 配合使用。這個類主要提供了加載圖片的方法和加載圖片時顯示的 Loading。
加載圖片的方法主要是:

- (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;
                         

這個方法主要用來加載圖片,其實 UIImageViewUIButton 加載圖片時最終會調用這個方法。這個方法會異步下載圖片并且添加緩存,這樣保證下次直接可以從緩存中讀取圖片。

參數(shù)說明:

url:圖片在服務器上的路徑;
placeholder:圖片加載時顯示的默認圖;
options:控制圖片的加載方式,關于更多的 SDWebImageOptions 將在下文講解
operationKey:操作(operation)的 key,如果為空時,將使用類名。這個主要使用來取消一個 opetion,結合 UIView+WebCacheOperation.h 使用;
setImageBlock:如果不想使用 SD 加載完圖片后顯示到視圖上,可以使用這個 Block 自定義加載圖片,這樣就可以在調用加載圖片的方法中加載圖片。它的完整定義是:

typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable imageData);

progress:進度回調,它的完整定義是,注意這里有一個 targetURL:

typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL);

completed:圖片加載完成后的回調,

typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL);

這里摘錄一段代碼,簡單講解一些,以下代碼主要用到的知識點有:

  • 位運算 &
  • 使用 NSOperation 下載圖片
  • 使用 runtime 給擴展添加屬性
  • 顯示加載 Loading
// 設置圖片時先取消以前的下載任務,這樣避免了復用圖片錯誤問題
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            // 設置默認圖
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        if ([self sd_showActivityIndicatorView]) {
            [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];
            if (!sself) {
                return;
            }
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    // 如果是自動設置圖,直接回調出去
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    // 設置圖片
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        // 如果圖片加載失敗,加載默認圖
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                // 回調出去
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        // 保存當前運行的 operation
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    }

例子主要展示直接使用 UIView 的擴展加載圖片,且使用 setImageBlock 加載圖片。只要理解了這個方法,那么關于 UIView 加載圖片基本上已經(jīng)掌握了:

[cell.sdimageView sd_internalSetImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:nil options:SDWebImageLowPriority operationKey:nil setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
        cell.sdimageView.image = image;
  } progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
            
 }];
        
  • UIView+WebCacheOperation.h

這個類主要用來記錄 UIView 加載 Operation 操作,大多數(shù)情況下一個 View 僅擁有
一個 Operation ,默認的 key 是當前類的類名,如果設置了不同的 key,將
保存不同個 Operation 。比如一個 UIButton,可以設置不同狀態(tài)下的圖片,那么我需要記錄多個 Operation 。它主要采用一個字典來保存所有的 Operation 。

operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

取消一個 Operation,這里需要注意 SDWebImageOperation。取消當前正在進行的 Operation。

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    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];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

  • UIImageView+WebCache.h
  • UIImageView+HighlightedWebCache.h
  • UIButton+WebCache.h

這幾個類主要是基于以下方法的進一步封裝,方便實用,這里就不做介紹了。

- (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;
  • UIImage+GIF.h

主要用來根據(jù) NSData 生成一個 GIF 圖片和一個判斷是否為 GIF 圖片。

  • UIImage+MultiFormat.h

主要用來根據(jù) NSData 生成不同格式的圖片,這里可能我們需要用到的是,根據(jù) Data 判斷圖片的格式。

下載操作

  • SDWebImageDownloaderOperation:NSOperation
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

這個文件可以說是整個 SD 的靈魂,它控制著圖片的下載過程,它與 NSOperationQueue 配合使用。關于更多 NSOperation 的介紹,近期會翻譯一篇文章來聊一聊 NSOperation。SDWebImageDownloaderOperationInterface:這是一個協(xié)議,可以自定義自己的 NSOperation,只要實現(xiàn)該協(xié)議中的方法,并且繼承自 NSOperation。

主要用到的知識點:

  • 使用 NSURLSession 下載
  • dispatch_barrier_async,dispatch_barrier_sync,dispatch_sync
  • 自定義 NSOperation
  • 網(wǎng)絡請求認證
  • 通知中心
  • 后臺任務

初始化:

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options NS_DESIGNATED_INITIALIZER;

使用這個方法來創(chuàng)建一個 SDWebImageDownloaderOperation,NS_DESIGNATED_INITIALIZER 這個宏說明所有的初始化方法最終都要調用這個方法,request 就是網(wǎng)絡請求的 request,session 當前 Operation 所在的 Session,options:SDWebImageDownloaderOptions,如何來下載任務,有一些枚舉值。

SDWebImageDownloader

這個類主要負責下載圖片,它是一個單例。它內部有 SDWebImageDownloadToken,用來標示一個下載任務,這樣根據(jù) token 來取消對應的任務。可以使用以下方法對 SDWebImageDownloader 進行初始化。當然如果想使用一個自定義的 NSURLSessionConfiguration,可以使用下面這個初始化方法:

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;

來初始化,下面是它的具體實現(xiàn):

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
    if ((self = [super init])) {
        // 下載的 Operation
        _operationClass = [SDWebImageDownloaderOperation class];
        _shouldDecompressImages = YES;
        _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
        
        // 下載對列,最大的并發(fā)數(shù)是6
        _downloadQueue = [NSOperationQueue new];
        _downloadQueue.maxConcurrentOperationCount = 6;
        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
        _URLOperations = [NSMutableDictionary new];
        
        // HTTP header
#ifdef SD_WEBP
        _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
        _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
        _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
        _downloadTimeout = 15.0;

        // NSURLSession
        sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;
        self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                     delegate:self
                                                delegateQueue:nil];
    }
    return self;
}

這是 SDWebImageDownloader 最終調用的初始化方法,主要配置了一些下載必備的數(shù)據(jù)。

下載方法:這個方法主要用來下載一個任務,下載任務使用的是 NSOperation + NSOperationQueue,來控制下載。也就是說這個方法主要生產(chǎn)一個 NSOperation ,并添加到 NSOperationQueue 中,這樣 NSOperationQueue 將自動管理下載任務。使用 NSOperation 的好處就是可以控制下載的整個過程,并且不需要管理線程的創(chuàng)建。當然它的優(yōu)點也就是它的缺點,只是使用場景的不同。

url:圖片下載的路徑
options:圖片下載的選項,它主要有下面這幾種選項:

  • SDWebImageDownloaderLowPriority = 1 << 0, 低優(yōu)先級
  • SDWebImageDownloaderProgressiveDownload = 1 << 1, 漸進式的下載,也就是一塊一塊的下載
  • SDWebImageDownloaderUseNSURLCache = 1 << 2, 默認情況不使用 URLCache,它與 NSURLRequestUseProtocolCachePolicy 對應,設置后使用 URLCache
  • SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
  • SDWebImageDownloaderContinueInBackground = 1 << 4, 后臺下載任務
  • SDWebImageDownloaderHandleCookies = 1 << 5, 它與 HTTPShouldHandleCookies 對應
  • SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, 允許不信任的 SSL 證書
  • SDWebImageDownloaderHighPriority = 1 << 7, 高優(yōu)先級下載
  • SDWebImageDownloaderScaleDownLargeImages = 1 << 8, 對下載后的圖片做處理

progress:進度回調,注意這個進度是在后臺線程執(zhí)行,刷新 UI 需要回到主線程
completed:下載完成后的回調
SDWebImageDownloadToken:返回值用這個來標示一個下載任務,取消的時候使用

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

// block 返回值是 SDWebImageDownloaderOperation,在 block 中創(chuàng)建一個 SDWebImageDownloaderOperation
 
    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        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,創(chuàng)建完成后添加到downloadQueue 中
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        // 處理 HTTP 認證的,大多情況不用處理
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        // 設置 Operation 的優(yōu)先級
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

        [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;
    }];
}

使用上面這個方法下載時,前提需要了解下面這個方法的實現(xiàn)。它使用一個字典緩存了所有的下載。使用 SDWebImageDownloadToken 來標記一個下載任務。

@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    // 如果 URL 為空直接回調,并返回
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }

    __block SDWebImageDownloadToken *token = nil;

    dispatch_barrier_sync(self.barrierQueue, ^{
    // 從緩存中取出 Operation
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {
            // 緩存不存在,調用 Block 創(chuàng)建一個新的 Operation
            operation = createCallback();
            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];
              };
            };
        }
        
        // 創(chuàng)建一個標記,并添加回調到緩存
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

以上就是下載的主要方法。還有一些設置屬性,很簡單,這里不作介紹。

緩存 SDImageCache

SD中的緩存主要采用了內存緩存(NSCache)加磁盤緩存(保存到沙河目錄中的 Cache 目錄下),SDImageCacheConfig 主要負責配置緩存。

初始化

directory:文件所要保存到沙河目錄,默認的是 Cache 目錄
ns:文件的域名,最終的路徑為:.../cache/om.hackemist.SDWebImageCache.ns
。需要注意的是所有的I/O操作都在一個串行對列中執(zhí)行。這里主要用到了文件的一些操作,比如文件大小,保存文件,文件路徑等。文件保存到沙盒時主要以文件的下載路徑,MD5后,加上文件后綴作為文件名,保存到本地和 NSCache 中。

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;

它監(jiān)聽了3個通知在初始化的時候:

  • UIApplicationDidReceiveMemoryWarningNotification:有內存警告時清除所有的緩存
  • UIApplicationWillTerminateNotification:刪除已過期的文件
  • UIApplicationDidEnterBackgroundNotification:在后臺刪除已過期的文件

當然可以使用單例初始化,使用默認的配置。
+ (nonnull instancetype)sharedImageCache;

SDWebImageManager

主要用來管理 SDImageCache 和 SDWebImageDownloader。也就是它把緩存和下載結合起來。

初始化:

這個方法是 SDWebImageManager 最終的初始化方法,也就是說所有的初始化方法最終都會調用這個方法,方便使用者自定義 SDWebImageManager,當然通常情況下使用單例方法初始化 + (nonnull instancetype)sharedManager;

- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader NS_DESIGNATED_INITIALIZER;
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        _imageCache = cache;
        _imageDownloader = downloader;
        // 下載失敗的 URL 緩存,注意它使用的是集合,這樣保證緩存中沒有重復的 URL
        _failedURLs = [NSMutableSet new];
        // 正在運行的操作
        _runningOperations = [NSMutableArray new];
    }
    return self;
}

下載一個圖片的主要方法:

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock

這里會將方法分成很多部分來講:

  • 1.參數(shù)異常判斷,保證程序的健壯性,一個好的程序,要處理好各種異常情況
// 使用斷言來保證完成的 Block 不能為空,也就是說如果你不需要完成回調,直接使用 SDWebImagePrefetcher 就行
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

// 保證 URL 是 NSString 類型,轉換成 NSURL 類型
if ([url isKindOfClass:NSString.class]) {
   url = [NSURL URLWithString:(NSString *)url];
}

// 保證 url 為 NSURL 類型
if (![url isKindOfClass:NSURL.class]) {
   url = nil;
}
  • 2.對 url 做異常處理,是否為不可使用的下載鏈接。SDWebImageCombinedOperation 是一個 NSObeject 對象。
 __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
 __weak SDWebImageCombinedOperation *weakOperation = operation;

// 判斷是否為下載失敗的 url
BOOL isFailedUrl = NO;
if (url) {
   // 保證線程安全
   @synchronized (self.failedURLs) {
       isFailedUrl = [self.failedURLs containsObject:url];
    }
}

// 如果是失敗的 url 且 operations 不為 SDWebImageRetryFailed,或者 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;
}
  • 3.保存當前的 Operation 到緩存
@synchronized (self.runningOperations) {
    [self.runningOperations addObject:operation];
}
// 獲取 url 對應的 Key
NSString *key = [self cacheKeyForURL:url];

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
        return @"";
    }
    // typedef NSString * _Nullable (^SDWebImageCacheKeyFilterBlock)(NSURL * _Nullable url);,cacheKeyFilter 是一個 Block,你可以自己設置 Cache 對應的 key
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        return url.absoluteString;
    }
}
    1. 從 Cache 中獲取圖片,它結合 option,進行不同的操作
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock
  • 4-1.如果 Operation 已經(jīng)取消,則移除,并結束程序的執(zhí)行
if (operation.isCancelled) {
    [self safelyRemoveOperationFromRunning:operation];
    return;
}
  • 4-2. 如果未能在緩存中找到圖片,或者強制刷新緩存,或者代理中未實現(xiàn)要強制下載圖片,那么它就需要下載圖片。
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {}

SDWebImageDownloaderOptions 根據(jù)不同的選項做不同的操作,根據(jù) SDWebImageOptions 轉換成對應的 SDWebImageDownloaderOptions。這里需要注意位運算,根據(jù)位運算可以計算出不同的選項。那么使用位定義的枚舉和用普通定義的枚舉值有什么優(yōu)缺點?需要讀者考慮。比如下面這兩種定義方法個的優(yōu)缺點。

SDWebImageDownloaderLowPriority = 1 << 0,

SDWebImageDownloaderLowPriority = 1,
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) {
  downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
  downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}

使用 imageDownloader 下載圖片,下載完成后保存到緩存,并移除 Operation。如果發(fā)生錯誤,,需要將失敗的 Url 保存到 failedURLs,避免實效的 Url 多次下載。這里需要注意一個 delegate ([self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]),它需要調用者自己實現(xiàn),這樣緩存中將保存轉換后的圖片。

SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished){

}
  • 4-3. 在緩存中找到圖片,直接返回

  • 4-4. 圖片不在緩存或者代理中不需要下載的,直接返回

SDWebImagePrefetcher

它是一個圖片預加載的類,你可以設置多個 URL。這種更適合哪些,在 wifi 情況下提前加載一些圖片,緩存起來,用戶使用的時候,直接從本地緩存中讀取。它實現(xiàn)起來也很簡單,使用一個遞歸來執(zhí)行每一個下載。它的本質使用的是 SDWebImageManager 處理下載,沒有使用單例,而新創(chuàng)建一個 manager。

初始化:

 (nonnull instancetype)initWithImageManager:(SDWebImageManager *)manager {
    if ((self = [super init])) {
        _manager = manager;
        _options = SDWebImageLowPriority;
        _prefetcherQueue = dispatch_get_main_queue();
        self.maxConcurrentDownloads = 3;
    }
    return self;
}

SDWebImagePrefetcherDelegate:

每下載完一個后,走一次回調

- (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didPrefetchURL:(nullable NSURL *)imageURL finishedCount:(NSUInteger)finishedCount totalCount:(NSUInteger)totalCount;

所有任務下載完后,執(zhí)行回調

- (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didFinishWithTotalCount:(NSUInteger)totalCount skippedCount:(NSUInteger)skippedCount;

SDWebImageCompat

由于 SD 會用到不同的平臺,需要做一些兼容性的處理。

NSData+ImageContentType

根據(jù) Data 來解析圖片的格式

SD 使用的知識點總結

  • GCD:

關于引用一段話:

Dispatch barriers 是一組函數(shù),在并發(fā)隊列上工作時扮演一個串行式的瓶頸。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個特定時間上是指定隊列上唯一被執(zhí)行的條目。這就意味著所有的先于調度障礙提交到隊列的條目必能在這個 Block 執(zhí)行前完成。

// 創(chuàng)建一個并行隊列
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

// 添加一個任務到對列中,使用 dispatch_barrier_async 添加的任務可以保存后添加
的任務依賴與前面添加過的任務,也就是說如果先前添加的任務還沒有執(zhí)行完成,那么后添加
的任務不會執(zhí)行,從而保證了線程安全。 
dispatch_barrier_async(self.barrierQueue, ^{
    [self.callbackBlocks addObject:callbacks];
});

// dispatch_sync 保證同步執(zhí)行方法,保證了線程安全
- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
    __block NSMutableArray<id> *callbacks = nil;
    dispatch_sync(self.barrierQueue, ^{
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
        [callbacks removeObjectIdenticalTo:[NSNull null]];
    });
    return [callbacks copy]; 
}

// dispatch_barrier_sync 保證同步執(zhí)行方法,保證了線程安全
- (BOOL)cancel:(nullable id)token {
    __block BOOL shouldCancel = NO;
    dispatch_barrier_sync(self.barrierQueue, ^{
        [self.callbackBlocks removeObjectIdenticalTo:token];
        if (self.callbackBlocks.count == 0) {
            shouldCancel = YES;
        }
    });
    if (shouldCancel) {
        [self cancel];
    }
    return shouldCancel;
}

// 回到主線程
dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});

// SD 的 cache 使用一個串行對列,控制線程的訪問
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

  • NSOperation:
    使用 NSOperation 更好的控制一個邏輯復雜的操作,可以控制它的整個操作過程,同時也不需要自己管理和創(chuàng)建線程。關于自定義 NSOperation,這里不做過多的解釋。不過使用 NSOperation 可以做到 Operation 之間的依賴,控制隊列中操作的最大并發(fā)數(shù),取消某個操作,而使用 GCD 的話做不到這一點。

  • NSURLSession:
    這是 iOS7 以后網(wǎng)絡請求類,它可以支持文件上傳,文件下載。

  • 使用 runtime 給某個已有的類添加屬性

static char TAG_ACTIVITY_STYLE;

- (void)sd_setIndicatorStyle:(UIActivityIndicatorViewStyle)style{
    objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN);
}

- (int)sd_getIndicatorStyle{
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue];
}
  • NSCache:
    內存緩存,如同字典一樣很好用。

SD 中的思想

  • 耦合度低,每個類負責不同的操作,相互之間可以獨立使用
  • 使用擴展,方便使用者
  • 異步下載圖片,并保存到內存與磁盤,提高系統(tǒng)性能
  • 保證主線程不被卡頓,提高性能
  • 通過一個 Manager 來控制不同的操作

時序圖

這張流程圖涵蓋了 SD 加載一張圖片時需要經(jīng)歷的過程:

流程圖

SD類圖

通過以上的學習,我們可以掌握各個類的作用,那么可以總結一下這張圖。

  • 所有的操作都圍繞在 SDWebImageManager;
  • SDWebImageManager 中包含了 SDImageCache 和 SDWebImageDownloader,來處理圖片的下載和緩存;
  • SDWebImageDownloader 使用 SDWebImageDownloaderOperation 執(zhí)行下載操作;
  • SDImageCache 使用 SDImageConfig 來配置緩存
  • 從 SDWebImageManager 衍生出一個預加載圖片的類 SDWebImagePrefetcher,負責多個圖片的預先加載
  • 底層封裝好通過擴展 UIView 讓視圖可以加載圖片

看懂這張圖需要明白 UML(Unified Modeling Language) 類圖:

  • 依賴關系(dependency):
    依賴關系是用一套帶箭頭的虛線表示的,UIButton(WebCache) 依賴于 UIView(WebCache);

它是一種臨時性的關系,通常在運行期間產(chǎn)生,并且隨著運行時的變化; 依賴關系也可能發(fā)生變化.顯然,依賴也有方向,雙向依賴是一種非常糟糕的結構,我們總是應該保持單向依賴,杜絕雙向依賴的產(chǎn)生;

  • 聚合關系(aggregation):聚合關系用一條帶空心菱形箭頭的直線表示,聚合關系用于表示實體對象之間的關系,表示整體由部分構成的語義;例如一個部門由多個員工組成;SDWebImagePrefetcher 由 SDWebImageManager 組成;

  • 實現(xiàn)關系(realize):實現(xiàn)關系用一條帶空心箭頭的虛線表示;比如 SDWebImageDownloaderOperation 實現(xiàn)了協(xié)議 SDWebImageOperation

  • 泛化關系(generalization):泛化關系用一條帶空心箭頭的實線表示,它是一種繼承關系。

整體架構

使用實例

  • 實例一:使用 UIView 的擴展加載圖片,并外部自動設置圖片
[cell.sdimageView sd_internalSetImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:nil options:SDWebImageLowPriority operationKey:nil setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
     cell.sdimageView.image = image;
} progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
            
}];
  • 實例二:預加載圖片
[SDWebImagePrefetcher sharedImagePrefetcher].delegate = self;
    [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:resultUrl progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
        
} completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
        
}];

總結

通過 SD 的深入學習,讓我了解到一個好的開源庫中使用的思想,深有體會,建議讀者也可以嘗試詳細讀一個開源庫。在讀 SD 的時候,需要把自己不懂的知識點,通過其它資料來掌握,這個過程收獲很大。前后大約花費了一周的時間(每天 1小時 30 分,大約),完成了這篇博客,如果有什么不合理的地方,讀者可以指出。深知寫博客需要一個長期堅持的過程,而付出很多自由的時間。所以我在看別人的博客時會特別認真的融入作者當時的思想中。那么 SD 中的思想究竟如何運用到我們的項目中呢?lefe 建議讀者可以從以下方面入手:

  • 解耦:模塊之間一定不要有太多的關聯(lián),我們往往對項目中的某個類做增量操作,不斷的給某個類添加新的代碼,導致這個類越來越重,我們試著把一個類拆分為不同的功能模塊;
  • 思路明確:從圖片的下載到圖片顯示到視圖上,要有明確的思路,先有一個大致的流程,然后逐步細化,逐步實現(xiàn);
  • 層次明確:應用層的使用不會印象到底層的設計;
  • GCD 和 NSOperation: 各有利弊,要合理的使用;
  • 注意性能:一定要注意性能,結合多線程,提升性能,比如 SD 讀取文件時會在一條線程中讀取;
  • 方便使用者:寫三方庫時,要讓用戶使用起來超級方便,比如在自己項目中寫項目組中公用的模塊時,要有明確的注釋,讓使用這更方便的使用;

參考

GCD

時序圖

類圖

如果您想第一時間看到我們的文章,歡迎關注公眾號。


微信公眾號

===== 我是有底線的 ======
喜歡我的文章,歡迎關注我的新浪微博 Lefe_x,我會不定期的分享一些開發(fā)技巧

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容