源碼淺析 SDWebImage 5.6

本文基於 SDWebImage 5.6。重讀的緣由也是因爲發現它的 API 在不斷迭代,許多結構已經不一樣與早期版本,同時也是爲了作一個記錄。閱讀順序也會依據 API 執行順序進行,不會太拘泥於細節,更可能是瞭解整個框架是如何運行的。c++

highlevel

5.x Migration Guid

若是你們有興趣的,強烈推薦觀看官方的推薦的遷移文檔,提到了5.x 版本的須要新特性,裏面詳細介紹其新特性和變化動機,主要 features:git

  • 全新的 Animated Image View (4.0 爲 FLAnimatedImageView);
  • 提供了 Image Transform 方便用戶在下載圖片後增長 scale, rotate, rounded corner 等操做;
  • Customization,能夠說一切皆協議,能夠 custom cacheloadercoder
  • 新增 View Indicator 來標識 Image 的 loading 狀態;

能夠說,5.x 的變化在於將整個 SDWebImage 中的核心類進行了協議化,同時將圖片的請求、加載、解碼、緩存等操做盡量的進行了插件化處理,達到方便擴展、可替換。github

協議化的類型不少,這裏僅列出一小部分:objective-c

4.4 5.x
SDWebImageCacheSerializerBlock id<SDWebImageCacheSerializer>
SDWebImageCacheKeyFilterBlock id<SDWebImageCacheKeyFilter>
SDWebImageDownloader id<SDImageLoader>
SDImageCache id<SDImageCache>
SDWebImageDownloaderProgressBlock id<SDWebImageIndicator>
FLAnimatedImageView id<SDAnimatedImage>

View Category

做爲上層 API 調用是經過在 UIView + WebCache 之上提供便利方法實現的,包含如下幾個 :數據庫

  • UIImageView+HighlightedWebCache
  • UIImageView+WebCache
  • UIView+WebCacheOperation
  • UIButton+WebCache
  • NSButton+WebCache

開始前,先來看看 SDWebImageCompat.h 它定義了SD_MAC、SD_UIKIT、SD_WATCH 這三個宏用來區分不一樣系統的 API 來知足條件編譯,同時還利用其來抹除 API 在不一樣平臺的差別,好比利用 #define UIImage NSImage 將 mac 上的 NSImage 統一爲 UIImage。另外值得注意的一點就是:設計模式

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif
複製代碼

區別於早起版本的實現:緩存

#define dispatch_main_async_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif
複製代碼
  • #ifndef 提升了代碼的嚴謹度,防止重複定義 dispatch_main_async_safe
  • 判斷條件由 isMainThread 改成了 dispatch_queue_t label 是否相等

關於第二點,有一篇 SD 的討論,以及另外一篇說明 GCD's Main Queue vs. Main Thread安全

Calling an API from a non-main queue that is executing on the main thread will lead to issues if the library (like VektorKit) relies on checking for execution on the main queue.markdown

區別就是從判斷是否在主線程執行改成是否在主隊列上調度。由於 在主隊列中的任務,必定會放到主線程執行session

相比 UIImageView 的分類,UIButton 須要存儲不一樣 UIControlState 和 backgrounImage 下的 image,Associate 了一個內部字典 (NSMutableDictionary<NSString *, NSURL *> *)sd_imageURLStorage 來保存圖片。

全部 View Category 的 setImageUrl: 最終收口到下面這個方法:

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock;
複製代碼

這個方法實現很長,簡單說明流程:

  1. SDWebImageContext 複製並轉換爲 immutable,獲取其中的 validOperationKey 值做爲校驗 id,默認值爲當前 view 的類名;
  2. 執行 sd_cancelImageLoadOperationWithKey 取消上一次任務,保證沒有當前正在進行的異步下載操做, 不會與即將進行的操做發生衝突;
  3. 設置佔位圖;
  4. 初始化 SDWebImageManagerSDImageLoaderProgressBlock , 重置 NSProgressSDWebImageIndicator;
  5. 開啓下載loadImageWithURL: 並將返回的 SDWebImageOperation 存入 sd_operationDictionary,key 爲 validOperationKey;
  6. 取到圖片後,調用 sd_setImage: 同時爲新的 image 添加 Transition 過渡動畫;
  7. 動畫結束後中止 indicator。

稍微說明的是 SDWebImageOperation它是一個 **strong - weak **的 NSMapTable,也是經過關聯值添加的:

// key is strong, value is weak because operation instance is retained by SDWebImageManager's runningOperations property
// we should use lock to keep thread-safe because these method may not be acessed from main queue
typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
複製代碼

用 weak 是由於 operation 實例是保存在 SDWebImageManager 的 runningOperations,這裏只是保存了引用,以方便 cancel 。

SDWebImageContext

A SDWebImageContext object which hold the original context options from top-level API.

image context 貫穿圖片處理的整個流程,它將數據逐級帶入各個處理任務中,存在兩種類型的 ImageContext:

typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM;
typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext;
typedef NSMutableDictionary<SDWebImageContextOption, id>SDWebImageMutableContext;
複製代碼

SDWebImageContextOption 是一個可擴展的 String 枚舉,目前有 15 種類型。基本上,你只需看名字也能猜出個大概,文檔,簡單作了以下分類:

image context

從其參與度來看,可見其重要性。

ImagePrefetcher

Prefetcher 它與 SD 整個處理流關係不大,主要用 imageManger 進行圖片批量下載,核心方法以下:

- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
                                          progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
                                         completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
複製代碼

它將下載的 URLs 做爲 事務 存入 SDWebImagePrefetchToken 中,避免以前版本在每次 prefetchURLs: 時將上一次的 fetching 操做 cancel 的問題。

每一個下載任務都是在 autoreleasesepool 環境下,且會用 SDAsyncBlockOperation 來包裝真正的下載任務,來達到任務的可取消操做:

@autoreleasepool {
    @weakify(self);
    SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) {
        @strongify(self);
        if (!self || asyncOperation.isCancelled) {
            return;
        }
        /// load Image ...
    }];
    @synchronized (token) {
        [token.prefetchOperations addPointer:(__bridge void *)prefetchOperation];
    }
    [self.prefetchQueue addOperation:prefetchOperation];
}
複製代碼

最後將任務存入 prefetchQueue,其最大限制下載數默認爲 3 。而 URLs 下載的真正任務是放在 token.loadOperations:

NSPointerArray *operations = token.loadOperations;
id<SDWebImageOperation> operation = [self.manager loadImageWithURL:url options:self.options context:self.context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
    /// progress handler    
}];
NSAssert(operation != nil, @"Operation should not be nil, [SDWebImageManager loadImageWithURL:options:context:progress:completed:] break prefetch logic");
@synchronized (token) {
    [operations addPointer:(__bridge void *)operation];
}
複製代碼

loadOperationsprefetchOperations 均使用 NSPointerArray ,這裏用到了其 NSPointerFunctionsWeakMemory 特性以及能夠存儲 Null 值,儘管其性能並非很好,參見:基礎集合類

另一個值得注意的是 PrefetchToken 對下載狀態的線程安全管理,使用了 c++11 memory_order_relaxed

atomic_ulong _skippedCount;
atomic_ulong _finishedCount;
atomic_flag  _isAllFinished;
    
unsigned long _totalCount;
複製代碼

即經過內存順序和原子操做作到無鎖併發,從而提升效率。具體原理感興趣的同窗能夠自行查閱資料。

ImageLoader

SDWebImageDownloader 是 <SDImageLoader> 協議在 SD 內部的默認實現。它提供了 HTTP/HTTPS/FTP 或者 local URL 的 NSURLSession 來源的圖片獲取能力。同時它最大程度的開放整個下載過程的的可配置性。主要 properties :

@interface SDWebImageDownloader : NSObject

@property (nonatomic, copy, readonly, nonnull) SDWebImageDownloaderConfig *config;
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderRequestModifier> requestModifier;
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderResponseModifier> responseModifier;
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderDecryptor> decryptor;
/* ... */

-(nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
    options:(SDWebImageDownloaderOptions)options
    context:(nullable SDWebImageContext *)context
   progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
  completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

@end
複製代碼

其中 downloaderConfig 是支持 NSCopy 協議的,提供的主要配置以下:

/// Defaults to 6.
@property (nonatomic, assign) NSInteger maxConcurrentDownloads;
/// Defaults to 15.0s.
@property (nonatomic, assign) NSTimeInterval downloadTimeout;
/// custom session configuration,不支持在使用過程當中動態替換類型; 
@property (nonatomic, strong, nullable) NSURLSessionConfiguration *sessionConfiguration;
/// 動態擴展類,須要遵循 `NSOperation<SDWebImageDownloaderOperation>` 以實現 SDImageLoader 定製
@property (nonatomic, assign, nullable) Class operationClass;
/// 圖片下載順序,默認 FIFO
@property (nonatomic, assign) SDWebImageDownloaderExecutionOrder executionOrder;
複製代碼

request modifier,提供在下載前修改 request,

/// Modify the original URL request and return a new one instead. You can modify the HTTP header, cachePolicy, etc for this URL.

@protocol SDWebImageDownloaderRequestModifier <NSObject>
   
- (nullable NSURLRequest *)modifiedRequestWithRequest:(nonnull NSURLRequest *)request;

@end
複製代碼

一樣,response modifier 則提供對返回值的修改,

/// Modify the original URL response and return a new response. You can use this to check MIME-Type, mock server response, etc.

@protocol SDWebImageDownloaderResponseModifier <NSObject>

- (nullable NSURLResponse *)modifiedResponseWithResponse:(nonnull NSURLResponse *)response;

@end
複製代碼

最後一個 decryptor 用於圖片解密,默認提供了對 imageData 的 base64 轉換,

/// Decrypt the original download data and return a new data. You can use this to decrypt the data using your perfereed algorithm.
@protocol SDWebImageDownloaderDecryptor <NSObject>

- (nullable NSData *)decryptedDataWithData:(nonnull NSData *)data response:(nullable NSURLResponse *)response;

@end
複製代碼

經過這個協議化後的對象來處理數據,能夠說是利用了設計模式中的 策略模式 或者 依賴注入。經過配置的方式獲取到協議對象,調用方僅需關心協議對象提供的方法,無需在乎其內部實現,達到解耦的目的。

###DownloadImageWithURL

下載前先檢查 URL 是否存在,沒有則直接拋錯返回。取到 URL 後嘗試複用以前生成的 operation:

NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
複製代碼

若是 operation 存在,調用

@synchronized (operation) {
    downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
複製代碼

並設置 queuePriority。這裏用了 @synchronized(operation) ,同時 Operation 內部則會用 @synchronized(self),以保證兩個不一樣類間 operation 的線程安全,由於 operation 有可能被傳遞到解碼或代理的隊列中。這裏 addHandlersForProgress: 會將 progressBlock 與 completedBlock 一塊兒存入 NSMutableDictionary<NSString *, id> SDCallbacksDictionary 而後返回保存在 downloadOperationCancelToken 中。

另外,Operation 在 addHandlersForProgress: 時並不會清除以前存儲的 callbacks 是增量保存的,也就是說屢次調用的 callBack 在完成後都會被依次執行。

若是 operation 不存在、任務被取消、任務已完成,調用 createDownloaderOperationWithUrl:options:context: 建立出新的 operation 並存儲在 URLOperations 中 。同時會配置 completionBlock,使得任務完成後能夠及時清理 URLOperations。保存 progressBlock 和 completedBlock;提交 operation 到 downloadQueue。

最終 operation、url、request、downloadOperationCancelToken 一塊兒被打包進 SDWebImageDownloadToken, 下載方法結束。

###CreateDownloaderOperation

下載結束,咱們來聊聊 operation 是如何建立的。首先是生成 URLRequest:

// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
mutableRequest.HTTPShouldUsePipelining = YES;
SD_LOCK(self.HTTPHeadersLock);
mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
SD_UNLOCK(self.HTTPHeadersLock);
複製代碼

主要經過 SDWebImageDownloaderOptions 獲取參數來配置, timeout 是由 downloader 的 config.downloadTimeout 決定,默認爲 15s。而後從 imageContext 中取出 id<SDWebImageDownloaderRequestModifier> requestModifier 對 request 進行改造。

// Request Modifier
id<SDWebImageDownloaderRequestModifier> requestModifier;
if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
    requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
} else {
    requestModifier = self.requestModifier;
}
複製代碼

值得注意的是 requestModifier 的獲取是有優先級的,經過 imageContext 獲得的優先級高於 downloader 所擁有的。經過這種方既知足了接口調用方可控,又能支持全局配置,可謂老小皆宜。同理,id<SDWebImageDownloaderResponseModifier> responseModifierid<SDWebImageDownloaderDecryptor> decryptor 也是如此。

以後會將確認過的 responseModifier 和 decryptor 再次保存到 imageContext 中爲以後使用。

最後,從 downloaderConfig 中取出 operationClass 建立 operation:

Class operationClass = self.config.operationClass;
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
    // Custom operation class
} else {
    operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
複製代碼

設置其 credential、minimumProgressInterval、queuePriority、pendingOperation。

默認狀況下,每一個任務是按照 FIFO 順序添加到 downloadQueue 中,若是用戶設置的是 LIFO 時,添加進隊列前會修改隊列中現有任務的優先級來達到效果:

if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
    // Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation
    // This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations
    // Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder
    for (NSOperation *pendingOperation in self.downloadQueue.operations) {
        [pendingOperation addDependency:operation];
    }
}
複製代碼

經過遍歷隊列,將新任務修改成當前隊列中全部任務的依賴以反轉優先級。

數據處理

SDWebImageDownloaderOperation 也是協議化後的類型,協議自己遵循 NSURLSessionTaskDelegate, NSURLSessionDataDelegate,它是真正處理 URL 請求數據的類,支持後臺下載,支持對 responseData 修改(by responseModifier),支持對 download ImageData 進行解密 (by decryptor)。其主要內部 properties 以下:

@property (assign, nonatomic, readwrite) SDWebImageDownloaderOptions options;
@property (copy, nonatomic, readwrite, nullable) SDWebImageContext *context;
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;

@property (strong, nonatomic, nullable) NSMutableData *imageData;
@property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse`
@property (assign, nonatomic) NSUInteger expectedSize; // may be 0
@property (assign, nonatomic) NSUInteger receivedSize;

@property (strong, nonatomic, nullable) id<SDWebImageDownloaderResponseModifier> responseModifier; // modifiy original URLResponse
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderDecryptor> decryptor; // decrypt image data
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;

@property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the queue to do image decoding
#if SD_UIKIT
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options
                                context:(nullable SDWebImageContext *)context;
複製代碼

初始化沒有什麼特別的,須要注意的是這裏傳入的 nullable session 是以 unownedSessin 保存,區別於內部默認生成的 ownedSession。若是初始化時 session 爲空,會在 start 時建立 ownedSession。

那麼問題來了,因爲咱們需觀察 session 的各個狀態,須要設置 delegate 來完成,

[NSURLSession sessionWithConfiguration:delegate:delegateQueue:];
複製代碼

ownedSession 的 delegate 毋庸置疑就在 operation 內部,而初始化傳入 session 的 delegate 則是 downloader 。它會經過 taskID 取出 operation 調用對應實現來完成回調的統一處理和轉發,例如:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
        [dataOperation URLSession:session task:task didCompleteWithError:error];
    }
}
複製代碼

接着做爲真正的消費者 operation 開始下載任務,整個下載過程包括開始、結束、取消都會發送對應通知。

  1. didReceiveResponse 時,會保存 response.expectedContentLength 做爲 expectedSize。而後調用 modifiedResponseWithResponse: 保存編輯後的 reponse。

  2. 每次 didReceiveData 會將 data 追加到 imageData:[self.imageData appendData:data] ,更新 receivedSizeself.receivedSize = self.imageData.length 。最終,當 receivedSize > expectedSize 斷定下載完成,執行後續處理。若是你支持了 SDWebImageDownloaderProgressiveLoad,每當收到數據時,將會進入 coderQueue 進行邊下載邊解碼:

// progressive decode the image in coder queue
dispatch_async(self.coderQueue, ^{
    @autoreleasepool {
        UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
        if (image) {
            // We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding.
            
            [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
        }
    }
});
複製代碼

​ 不然,會在 didCompleteWithError 時完成解碼操做:SDImageLoaderDecodeImageData ,不過在解碼前須要先解密:

if (imageData && self.decryptor) {
    imageData = [self.decryptor decryptedDataWithData:imageData response:self.response];
}
複製代碼

​ 3. 處理 complete 回調;

關於 decode 的邏輯咱們最後聊。

ImageCache

基本上 Cache 相關類的設計思路與 ImageLoader 一致,會有一份 SDImageCacheConfig 以配置緩存的過時時間,容量大小,讀寫權限,以及動態可擴展的 MemoryCache/DiskCache。

SDImageCacheConfig 主要屬性以下:

@property (assign, nonatomic) BOOL shouldDisableiCloud;
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
@property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenEnterBackground;
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
@property (assign, nonatomic) NSTimeInterval maxDiskAge;
@property (assign, nonatomic) NSUInteger maxDiskSize;
@property (assign, nonatomic) NSUInteger maxMemoryCost;
@property (assign, nonatomic) NSUInteger maxMemoryCount;
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;
/// Defaults to built-in `SDMemoryCache` class.
@property (assign, nonatomic, nonnull) Class memoryCacheClass;
/// Defaults to built-in `SDDiskCache` class.
@property (assign ,nonatomic, nonnull) Class diskCacheClass;
複製代碼

MemoryCache、DiskCache 的實例化都須要 SDImageCacheConfig 的傳入:

/// SDMemoryCache
- (nonnull instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;
/// SDDiskCache
- (nullable instancetype)initWithCachePath:(nonnull NSString *)cachePath config:(nonnull SDImageCacheConfig *)config;
複製代碼

做爲緩存協議,他們的接口聲明基本一致,都是對數據的 CURD,區別在於 MemoryCache Protocl 操做的是 id 類型 (NSCache API 限制),DiskCache 則是對 NSData。

咱們來看看他們的默認實現吧。

SDMemoryCache

/**
 A memory cache which auto purge the cache on memory warning and support weak cache.
 */
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType> <SDMemoryCache>

@property (nonatomic, strong, nonnull, readonly) SDImageCacheConfig *config;

@end
複製代碼

內部就是將 NSCache 擴展爲了 SDMemoryCache 協議,並加入了 *NSMapTable<KeyType, ObjectType> weakCache ,併爲其添加了信號量鎖來保證線程安全。這裏的 weak-cache 是僅在 iOS/tvOS 平臺添加的特性,由於在 macOS 上儘管收到系統內存警告,NSCache 也不會清理對應的緩存。weakCache 使用的是 strong-weak 引用不會有有額外的內存開銷且不影響對象的生命週期。

weakCache 的做用在於恢復緩存,它經過 CacheConfig 的 shouldUseWeakMemoryCache 開關以控制,詳細說明能夠查看 CacheConfig.h。先看看其如何實現的:

- (id)objectForKey:(id)key {
    id obj = [super objectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache) {
        return obj;
    }
    if (key && !obj) {
        // Check weak cache
        SD_LOCK(self.weakCacheLock);
        obj = [self.weakCache objectForKey:key];
        SD_UNLOCK(self.weakCacheLock);
        if (obj) {
            // Sync cache
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[UIImage class]]) {
                cost = [(UIImage *)obj sd_memoryCost];
            }
            [super setObject:obj forKey:key cost:cost];
        }
    }
    return obj;
}
複製代碼

因爲 NSCache 遵循 NSDiscardableContent 策略來存儲臨時對象的,當內存緊張時,緩存對象有可能被系統清理掉。此時,若是應用訪問 MemoryCache 時,緩存一旦未命中,則會轉入 diskCache 的查詢操做,可能致使 image 閃爍現象。而當開啓 shouldUseWeakMemoryCache 時,由於 weakCache 保存着對象的弱引用 (在對象 被 NSCache 被清理且沒有被釋放的狀況下),咱們可經過 weakCache 取到緩存,將其塞會 NSCache 中。從而減小磁盤 I/O。

SDDiskCache

這個更簡單,內部使用 NSFileManager 管理圖片數據讀寫, 調用 SDDiskCacheFileNameForKey 將 key MD5 處理後做爲 fileName,存放在 diskCachePath 目錄下。另外就是過時緩存的清理:

  1. 根據 SDImageCacheConfigExpireType 排序獲得 NSDirectoryEnumerator *fileEnumerator ,開始過濾;
  2. 以 cacheConfig.maxDiskAage 對比判斷是否過時,將過時 URL 存入 urlsToDelete;
  3. 調用 [self.fileManager removeItemAtURL:fileURL error:nil];
  4. 根據 cacheConfig.maxDiskSize 來刪除磁盤緩存的數據,清理到 maxDiskSize 的 1/2 爲止。

另一點就是 SDDiskCache 同 YYKVStorage 同樣一樣支持爲 UIImage 添加 extendData 用以存儲額外信息,例如,圖片的縮放比例, URL rich link, 時間等其餘數據。

不過 YYKVStorage 自己是用數據庫中 manifest 表的 extended_data 字段來存儲的。SDDiskCache 就另闢蹊徑解決了。利用系統 API <sys/xattr.h> 的 setxattrgetxattrlistxattr 將 extendData 保存。能夠說又漲姿式了。順便說一下,它對應的 key 是用 SDDiskCacheExtendedAttributeName

SDImageCache

也是協議化後的類,負責調度 SDMemoryCache、SDDiskCache,其 Properties 以下:

@property (nonatomic, strong, readwrite, nonnull) id<SDMemoryCache> memoryCache;
@property (nonatomic, strong, readwrite, nonnull) id<SDDiskCache> diskCache;
@property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config;
@property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath;
@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;
複製代碼

說明:memoryCache 和 diskCache 實例是依據 CacheConfig 中定義的 class 來生成的,默認爲 SDMemoryCache 和 SDDiskCache。

咱們看看其核心方法:

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
          toMemory:(BOOL)toMemory
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;
複製代碼
  1. 確保 image 和 key 存在;

  2. shouldCacheImagesInMemory 爲 YES,則會調用 [self.memoryCache setObject:image forKey:key cost:cost] 進行 memoryCache 寫入;

  3. 進行 diskCache 寫入,操做邏輯放入 ioQueue 和 autoreleasepool 中。

    dispatch_async(self.ioQueue, ^{
        @autoreleasepool {
            NSData *data = ... // 根據 SDImageFormat 對 image 進行編碼獲取
            /// data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
            [self _storeImageDataToDisk:data forKey:key];
            if (image) {
                // Check extended data
                id extendedObject = image.sd_extendedObject;
                // ... get extended data
                [self.diskCache setExtendedData:extendedData forKey:key];
            }
        }
        // call completionBlock in main queue
    });
    複製代碼

另外一個重要的方法就是 image query,定義在 SDImageCache 協議中:

- (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock {
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData;
    if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync;
    if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage;
    if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
    if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames;
    if (options & SDWebImageMatchAnimatedImageClass) cacheOptions |= SDImageCacheMatchAnimatedImageClass;
    
    return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock];
}
複製代碼

它只作了一件事情,將 SDWebImageOptions 轉換爲 SDImageCacheOptions,而後調用 queryCacheOperationForKey: ,其內部邏輯以下:

首先,若是 query key 存在,會從 imageContext 中獲取 transformer,對 query key 進行轉換:

key = SDTransformedKeyForKey(key, transformerKey);
複製代碼

嘗試從 memory cache 獲取 image,若是存在:

  1. 知足 SDImageCacheDecodeFirstFrameOnly 且遵循 SDAnimatedImage 協議,則會取出 CGImage 進行轉換

    // Ensure static image
    Class animatedImageClass = image.class;
    if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
    #if SD_MAC
        image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
    #else
        image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
    #endif
    }
    複製代碼
  2. 知足 SDImageCacheMatchAnimatedImageClass ,則會強制檢查 image 類型是否匹配,不然將數據至 nil:

    // Check image class matching
    Class animatedImageClass = image.class;
    Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
    if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
        image = nil;
    }
    複製代碼

當能夠從 memory cache 獲取到 image 且爲 SDImageCacheQueryMemoryData,直接完成返回,不然繼續;

開始 diskCache 讀取,依據讀取條件斷定 I/O 操做是否爲同步。

// Check whether we need to synchronously query disk
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                            (!image && options & SDImageCacheQueryDiskDataSync));
複製代碼

整個 diskQuery 存在 queryDiskBlock 中並用 autorelease 包裹:

void(^queryDiskBlock)(void) =  ^{
    if (operation.isCancelled) {
        // call doneBlock & return
    }
    @autoreleasepool {
        NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        UIImage *diskImage;
        SDImageCacheType cacheType = SDImageCacheTypeNone;
        if (image) {
            // the image is from in-memory cache, but need image data
            diskImage = image;
            cacheType = SDImageCacheTypeMemory;
        } else if (diskData) {
            cacheType = SDImageCacheTypeDisk;
            // decode image data only if in-memory cache missed
            diskImage = [self diskImageForKey:key data:diskData options:options context:context];
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = diskImage.sd_memoryCost;
                [self.memoryCache setObject:diskImage forKey:key cost:cost];
            }
        }
        // call doneBlock
        if (doneBlock) {
            if (shouldQueryDiskSync) {
                doneBlock(diskImage, diskData, cacheType);
            } else {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, cacheType);
                });
            }
        }
    }
}
複製代碼

對於大量臨時內存操做 SD 都會將其放入 autoreleasepool 以保證內存能及時被釋放。

特別強調,代碼若是執行到這,就必定會有磁盤讀取到操做,所以,若是不是非要獲取 imageData 能夠經過 SDImageCacheQueryMemoryData 來提升查詢效率;

最後,SDTransformedKeyForKey 的轉換邏輯是以 SDImageTransformer 的 transformerKey 按順序依次拼接在 image key 後面。例如:

'image.png' |> flip(YES,NO) |> rotate(pi/4,YES)  => 
'image-SDImageFlippingTransformer(1,0)-SDImageRotationTransformer(0.78539816339,1).png'
複製代碼

SDImageManaer

SDImageManger 做爲整個庫的調度中心,上述各類邏輯的集大成者,它把各個組建串聯,從視圖 > 下載 > 解碼器 > 緩存。而它暴露的核心方法就一個,就是 loadImage:

@property (strong, nonatomic, readonly, nonnull) id<SDImageCache> imageCache;
@property (strong, nonatomic, readonly, nonnull) id<SDImageLoader> imageLoader;
@property (strong, nonatomic, nullable) id<SDImageTransformer> transformer;
@property (nonatomic, strong, nullable) id<SDWebImageCacheKeyFilter> cacheKeyFilter;
@property (nonatomic, strong, nullable) id<SDWebImageCacheSerializer> cacheSerializer;
@property (nonatomic, strong, nullable) id<SDWebImageOptionsProcessor> optionsProcessor;

@property (nonatomic, class, nullable) id<SDImageCache> defaultImageCache;
@property (nonatomic, class, nullable) id<SDImageLoader> defaultImageLoader;

- (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                                 completed:(nonnull SDInternalCompletionBlock)completedBlock;
複製代碼

這裏先簡單說一下 cacheKeyFilter、cacheSerializer 和 optionsProcessor 這三個 API,其他的上面都提到過了。

SDWebImageCacheKeyFilter

默認狀況下,是把 URL.absoluteString 做爲 cacheKey ,而若是設置了 fileter 則會對經過 cacheKeyForURL: 對 cacheKey 攔截並進行修改;

SDWebImageCacheSerializer

默認狀況下,ImageCache 會直接將 downloadData 進行緩存,而當咱們使用其餘圖片格式進行傳輸時,例如 WEBP 格式的,那麼磁盤中的存儲則會按 WEBP 格式來。這會產生一個問題,每次當咱們須要從磁盤讀取 image 時都須要進行重複的解碼操做。而經過 CacheSerializer 能夠直接將 downloadData 轉換爲 JPEG/PNG 的格式的 NSData 緩存,從而提升訪問效率。

SDWebImageOptionsProcessor

用於控制全局的 SDWebImageOptions 和 SDWebImageContext 中的參數。示例以下:

SDWebImageManager.sharedManager.optionsProcessor = [SDWebImageOptionsProcessor optionsProcessorWithBlock:^SDWebImageOptionsResult * _Nullable(NSURL * _Nullable url, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
     // Only do animation on `SDAnimatedImageView`
     if (!context[SDWebImageContextAnimatedImageClass]) {
        options |= SDWebImageDecodeFirstFrameOnly;
     }
     // Do not force decode for png url
     if ([url.lastPathComponent isEqualToString:@"png"]) {
        options |= SDWebImageAvoidDecodeImage;
     }
     // Always use screen scale factor
     SDWebImageMutableContext *mutableContext = [NSDictionary dictionaryWithDictionary:context];
     mutableContext[SDWebImageContextImageScaleFactor] = @(UIScreen.mainScreen.scale);
     context = [mutableContext copy];
 
     return [[SDWebImageOptionsResult alloc] initWithOptions:options context:context];
 }];
複製代碼

LoadImage

接口的的第一個參數 url 做爲整個框架的鏈接核心,卻設計成 nullable 應該徹底是方便調用方而設計的。內部經過對 url 的 nil 判斷以及對 NSString 類型的兼容 (強制轉成 NSURL) 以保證後續的流程,不然結束調用。下載開始後又拆分紅了一下 6 個方法:

  • callCacheProcessForOperation
  • callDownloadProcessForOperation
  • callStoreCacheProcessForOperation
  • callTransformProcessForOperation
  • callCompletionBlockForOperation
  • safelyRemoveOperationFromRunning

分別是:緩存查詢、下載、存儲、轉換、執行回調、清理回調。你能夠發現每一個方法都是針對 operation 的操做,operation 在 loadImage 時會準備好,而後開始緩存查詢。

SDWebImageCombinedOperation *operation = [SDWebImagCombinedOperation new];
operation.manager = self;

///  1
BOOL isFailedUrl = NO;
if (url) {
    SD_LOCK(self.failedURLsLock);
    isFailedUrl = [self.failedURLs containsObject:url];
    SD_UNLOCK(self.failedURLsLock);
}

if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
    [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
    return operation;
}

SD_LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(self.runningOperationsLock);

// 2. Preprocess the options and context arg to decide the final the result for manager
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
複製代碼

loadImage 方法自己不復雜,核心是生成 operation 而後轉入緩存查詢。

在 operation 初始化後會檢查 failedURLs 是否包含當前 url:

  • 若是有且 options 爲 SDWebImageRetryFailed,直接結束並返回 operation;
  • 若是檢查經過會將 operation 存入 runningOperations 中。並將 options 和 imageContext 封入 SDWebImageOptionsResult。

同時,會更新一波 imageContext,主要先將 transformer、cacheKeyFilter、cacheSerializer 存入 imageContext 作爲全局默認設置,再調用 optionsProcessor 來提供用戶的自定義 options 再次加工 imageContext 。這個套路你們應該有印象吧,前面的 ImageLoader 中的 requestModifer 的優先級邏輯與此相似,不過實現方式有些差別。最後轉入 CacheProcess。

loadImage 過程是使用了 combineOperation,它是 combine 了 cache 和 loader 的操做任務,使其能夠一步到位清理緩存查詢和下載任務的做用。其聲明以下:

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
/// imageCache queryImageForKey: 的 operation
@property (strong, nonatomic, nullable, readonly) id<SDWebImageOperation> cacheOperation;
/// imageLoader requestImageWithURL: 的 operation
@property (strong, nonatomic, nullable, readonly) id<SDWebImageOperation> loaderOperation;
/// Cancel the current operation, including cache and loader process
- (void)cancel;
@end
複製代碼

其提供的 cancel 方法會逐步檢查兩種類型 opration 而後逐一執行 cancel 操做。

####CallCacheProcessForOperation

先檢查 SDWebImageFromLoaderOnly 值,判斷是否爲直接下載的任務,

是,則轉到 downloadProcess。

否,則經過 imageCache 建立查詢任務並將其保存到 combineOperation 的 cacheOperation :

operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
   if (!operation || operation.isCancelled) {
    	/// 1  
   }
  	/// 2
}];
複製代碼

對緩存查詢的結果有兩種狀況須要處理:

  1. 當隊列執行到該任務時,若是 operaton 被標誌爲 canceled 狀態則結束下載任務;
  2. 不然轉到 downloadProcess 。

####CallDownloadProcessForOperation

下載的實現比較複雜,首先須要決定是否須要新建下載任務,由三個變量控制:

BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
    shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
    shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
    shouldDownload &= [self.imageLoader canRequestImageForURL:url];
複製代碼
  • 檢查 options 值是否爲 SDWebImageFromCacheOnly 或 SDWebImageRefreshCached 的
  • 由代理決定是否須要新建下載任務
  • 經過 imageLoader 控制可否支持下載任務
  1. 若是 shouldDownload 爲 NO,則結束下載並調用 callCompletionBlockForOperationsafelyRemoveOperationFromRunning。此時若是存在 cacheImage 則會隨 completionBlock 一塊兒返回。

  2. 若是 shouldDownload 爲 YES,新建下載任務並將其保存在 combineOperation 的 loaderOperation。在新建任務前,若有取到 cacheImage 且 SDWebImageRefreshCached 爲 YES,會將其存入 imageContext (沒有則建立 imageContext)。

  3. 下載結束後回到 callBack,這裏會先處理幾種狀況:

    • operation 被 cancel 則拋棄下載的 image、data ,callCompletionBlock 結束下載;
    • reqeust 被 cancel 致使的 error,callCompletionBlock 結束下載;
    • imageRefresh 後請求結果仍舊命中了 NSURLCache 緩存,則不會調用 callCompletionBlock;
    • errro 出錯,callCompletionBlockForOperation 並將 url 添加至 failedURLs;
    • 均無以上狀況,若是是經過 retry 成功的,會先將 url 從 failedURLs 中移除,調用 storeCacheProcess;

    最後會對標記爲 finished 的執行 safelyRemoveOperation;

####CallStoreCacheProcessForOperation

先從 imageContext 中取出 storeCacheType、originalStoreCacheType、transformer、cacheSerializer,判斷是否須要存儲轉換後圖像數據、原始數據、等待緩存存儲結束:

BOOL shouldTransformImage = downloadedImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer;
BOOL shouldCacheOriginal = downloadedImage && finished;
BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
複製代碼

若是 shouldCacheOriginal 爲 NO,直接轉入 transformProcess。不然,先確認存儲類型是否爲原始數據:

// normally use the store cache type, but if target image is transformed, use original store cache type instead
SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
複製代碼

存儲時若是 cacheSerializer 存在則會先轉換數據格式,最終都調用 [self stroageImage: ...]

當存儲結束時,轉入最後一步,transformProcess。

####CallTransformProcessForOperation

轉換開始前會例行判斷是否須要轉換,爲 false 則 callCompletionBlock 結束下載,判斷以下:

id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
BOOL shouldTransformImage = originalImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer;
BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
複製代碼

若是須要轉換,會進入全局隊列開始處理:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    @autoreleasepool {
        UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
        if (transformedImage && finished) {
				/// 1
        } else {
				callCompletionBlock
        }
    }
});        
複製代碼

轉換成功後,會依據 cacheData = [cacheSerializer cacheDataWithImage: originalData: imageURL:]; 進行 [self storageImage: ...]存儲圖片。存儲結束後 callCompletionBlock。

總結

若是你能看到這裏,仍是頗有耐心的。但願你們看完可以大概瞭解 SD 的 work-flow,以及一些細節上的處理和思考。在 SD 5.x 中,我的感覺最多的是其架構的設計值得借鑑。

  • 如何設計一個穩定可擴展的 API 又能安全地支持動態添加參數?
  • 若是設計一個解耦又可動態插拔的架構?

最後,這篇其實還少了 SDImageCoder,這個留到下一篇的 SDWebImage 插件及其擴展上來講。

相關文章
相關標籤/搜索