SDWebImage是一個圖片下載的開源項目,由于它提供了簡介的接口以及異步下載與緩存的強大功能,深受“猿媛“的喜愛。截止到本篇文章開始,項目的star數已經超過1.6k了。今天我就對項目的源碼做個閱讀筆記,一方面歸納總結自己的心得,另一方面給準備閱讀源碼的童鞋做點鋪墊工作。代碼最新版本為3.8。
正如項目的第一句介紹一樣:
Asynchronous image downloader with cache support as a UIImageView category
SDWebImage
是個支持異步下載與緩存的UIImageView擴展。項目主要提供了一下功能:
- 擴展UIImageView, UIButton, MKAnnotationView,增加網絡圖片與緩存管理。
- 一個異步的圖片加載器
- 一個異步的 內存 + 磁盤 圖片緩存,擁有自動的緩存過期處理機制。
- 支持后臺圖片解壓縮處理
- 確保同一個 URL 的圖片不被多次下載
- 確保虛假的 URL 不會被反復加載
- 確保下載及緩存時,主線程不被阻塞
- 使用 GCD 與 ARC
項目支持的圖片格式包括PNG,JEPG,GIF,WebP等等。
先看看SDWebImage
的項目組織架構:
SDWebImageDownloader負責維持圖片的下載隊列;
SDWebImageDownloaderOperation負責真正的圖片下載請求;
SDImageCache負責圖片的緩存;
SDWebImageManager是總的管理類,維護了一個SDWebImageDownloader
實例和一個SDImageCache
實例,是下載與緩存的橋梁;
SDWebImageDecoder負責圖片的解壓縮;
SDWebImagePrefetcher負責圖片的預取;
UIImageView+WebCache和其他的擴展都是與用戶直接打交道的。
其中,最重要的三個類就是SDWebImageDownloader
、SDImageCache
、SDWebImageManager
。接下來我們就分別詳細地研究一下這些類各自具體做了哪些事,又是怎么做的。
為了便于大家從宏觀上有個把握,我這里先給出項目的框架結構:
UIImageView+WebCache
和UIButton+WebCache
直接為表層的 UIKit框架提供接口, 而 SDWebImageManger
負責處理和協調SDWebImageDownloader
和SDWebImageCache
, 并與 UIKit層進行交互。SDWebImageDownloaderOperation
真正執行下載請求;最底層的兩個類為高層抽象提供支持。
我們按照從上到下執行的流程來研究各個類
UIImageView+WebCache
這里,我們只用UIImageView+WebCache
來舉個例子,其他的擴展類似。
常用的場景是已知圖片的url地址,來下載圖片并設置到UIImageView上。UIImageView+WebCache
提供了一系列的接口:
- (void)setImageWithURL:(NSURL *)url;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)setImageWithURL:(NSURL *)url completed:(SDWebImageCompletedBlock)completedBlock;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletedBlock)completedBlock;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletedBlock)completedBlock;
這些接口最終會調用
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
方法的第一行代碼[self sd_cancelCurrentImageLoad]
是取消UIImageView上當前正在進行的異步下載,確保每個 UIImageView 對象中永遠只存在一個 operation,當前只允許一個圖片網絡請求,該 operation 負責從緩存中獲取 image 或者是重新下載 image。具體執行代碼是:
// UIView+WebCacheOperation.m
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey: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];
}
實際上,所有的操作都是由一個operationDictionary
字典維護的,執行新的操作之前,先cancel所有的operation。這里的cancel是SDWebImageOperation
協議里面定義的。
//預覽 占位圖
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
是一種占位圖策略,作為圖片下載完成之前的替代圖片。dispatch_main_async_safe
是一個宏,保證在主線程安全執行,最后再講。
然后判斷url,url為空就直接調用完成回調,報告錯誤信息;否則,用SDWebImageManager
單例的
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
方法下載圖片。下載完成之后刷新UIImageView的圖片。
//圖像的繪制只能在主線程完成
dispatch_main_sync_safe(^{
if (!wself) return;
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{//延遲設置圖片,手動處理
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
//直接設置圖片
wself.image = image;
[wself setNeedsLayout];
} else {
//image== nil,設置占位圖
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
最后,把返回的id <SDWebImageOperation> operation
添加到operationDictionary
中,方便后續的cancel。
SDWebImageManager
在SDWebImageManager.h
中是這樣描述SDWebImageManager
類的:
The SDWebImageManager is the class behind the UIImageView+WebCache category and likes.It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache).You can use this class directly to benefit from web image downloading with caching in another context than a UIView.
即隱藏在UIImageView+WebCache
背后,用于處理異步下載和圖片緩存的類,當然你也可以直接使用 SDWebImageManager 的方法 downloadImageWithURL:options:progress:completed:
來直接下載圖片。
SDWebImageManager.h
首先定義了一些枚舉類型的SDWebImageOptions
。關于這些Options的具體含義可以參考葉孤城大神的解析
然后,聲明了三個block:
//操作完成的回調,被上層的擴展調用。
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);
//被SDWebImageManager調用。如果使用了SDWebImageProgressiveDownload標記,這個block可能會被重復調用,直到圖片完全下載結束,finished=true,再最后調用一次這個block。
typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);
//SDWebImageManager每次把URL轉換為cache key的時候調用,可以刪除一些image URL中的動態部分。
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);
定義了SDWebImageManagerDelegate
協議:
@protocol SDWebImageManagerDelegate <NSObject>
@optional
/**
* Controls which image should be downloaded when the image is not found in the cache.
*
* @param imageManager The current `SDWebImageManager`
* @param imageURL The url of the image to be downloaded
*
* @return Return NO to prevent the downloading of the image on cache misses. If not implemented, YES is implied.
* 控制在cache中沒有找到image時 是否應該去下載。
*/
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
/**
* Allows to transform the image immediately after it has been downloaded and just before to cache it on disk and memory.
* NOTE: This method is called from a global queue in order to not to block the main thread.
*
* @param imageManager The current `SDWebImageManager`
* @param image The image to transform
* @param imageURL The url of the image to transform
*
* @return The transformed image object.
* 在下載之后,緩存之前轉換圖片。在全局隊列中操作,不阻塞主線程
*/
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
@end
SDWebImageManager
是單例使用的,分別維護了一個SDImageCache
實例和一個SDWebImageDownloader
實例。 類方法分別是:
//初始化SDWebImageManager單例,在init方法中已經初始化了cache單例和downloader單例。
- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader;
//下載圖片
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
//緩存給定URL的圖片
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
//取消當前所有的操作
- (void)cancelAll;
//監測當前是否有進行中的操作
- (BOOL)isRunning;
//監測圖片是否在緩存中, 先在memory cache里面找 再到disk cache里面找
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
//監測圖片是否緩存在disk里
- (BOOL)diskImageExistsForURL:(NSURL *)url;
//監測圖片是否在緩存中,監測結束后調用completionBlock
- (void)cachedImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//監測圖片是否緩存在disk里,監測結束后調用completionBlock
- (void)diskImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//返回給定URL的cache key
- (NSString *)cacheKeyForURL:(NSURL *)url;
我們主要研究
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
首先,監測url 的合法性:
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
第一個判斷條件是防止很多用戶直接傳遞NSString作為NSURL導致的錯誤,第二個判斷條件防止crash。
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
集合failedURLs
保存之前失敗的urls,如果url為空或者url之前失敗過且不采用重試策略,直接調用completedBlock返回錯誤。
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
runningOperations
是一個可變數組,保存所有的operation,主要用來監測是否有operation在執行,即判斷running 狀態。
SDWebImageManager
會首先在memory以及disk的cache中查找是否下載過相同的照片,即調用imageCache的
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock
方法。
如果在緩存中找到圖片,直接調用completedBlock,第一個參數是緩存的image。
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !strongOperation.isCancelled) {//為啥這里用strongOperation TODO
completedBlock(image, nil, cacheType, YES, url);
}
});
如果沒有在緩存中找到圖片,或者不管是否找到圖片,只要operation有SDWebImageRefreshCached
標記,那么若SDWebImageManagerDelegate
的shouldDownloadImageForURL
方法返回true,即允許下載時,都使用 imageDownloader 的
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
方法進行下載。如果下載有錯誤,直接調用completedBlock返回錯誤,并且視情況將url添加到failedURLs里面;
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
if (error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
如果下載成功,若支持失敗重試,將url從failURLs里刪除:
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
如果delegate實現了,imageManager:transformDownloadedImage:withURL:
方法,圖片在緩存之前,需要做轉換(在全局隊列中調用,不阻塞主線程)。轉化成功切下載全部結束,圖片存入緩存,調用completedBlock回調,第一個參數是轉換后的image。
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];
//將圖片緩存起來
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
否則,直接存入緩存,調用completedBlock回調,第一個參數是下載的原始image。
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
存入緩存都是調用imageCache的
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk
方法。
如果沒有在緩存找到圖片,且不允許下載,直接調用completedBlock,第一個參數為nil。
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !weakOperation.isCancelled) {//為啥這里用weakOperation TODO
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
最后都要將這個operation從runningOperations里刪除。
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
這里再說一下上面的operation,是一個SDWebImageCombinedOperation
實例:
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;
@end
是一個遵循SDWebImageOperation
協議的NSObject子類。
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
在里面封裝一個NSOperation,這么做的目的應該是為了使代碼更簡潔。因為下載操作需要查詢緩存的operation和實際下載的operation,這個類的cancel方法可以同時cancel兩個operation,同時還可以維護一個狀態cancelled。
敬請期待后續更新!