前言
SDWebImage作為一個(gè)加載圖片開源庫(kù),在大多數(shù)項(xiàng)目里它和一些刷新、彈窗控件一樣普及。而且這個(gè)東西寫得很好,網(wǎng)上已經(jīng)有很多篇對(duì)此庫(kù)剖析的文章了。正好這段時(shí)間有時(shí)間,我也花了好多天時(shí)間認(rèn)真閱讀了下它的代碼。確實(shí)感覺受益頗多,因此就專門寫篇筆記,一來再次梳理下思路,加深理解。二來也是為方便日后溫習(xí),溫故而知新。
好了,開始。
分析
我自己畫了一張圖,展示了SDWebImage在加載圖片時(shí)的整個(gè)脈絡(luò)結(jié)構(gòu)。先將此圖亮出,下面邊分析邊回過頭來對(duì)照該圖加深理解。

我們一般在在項(xiàng)目中是這樣使用它的:
UIImage *image = [UIImage imageNamed:@"1.jpg"];
NSString *urlStr = @"http://dn-edu-test.qbox.me/5436557847300726784";
NSURL *url = [NSURL URLWithString:urlStr];
[_imgV setImageWithURL:url placeholderImage:image];
SDWebImage提供的接口方法非常簡(jiǎn)單,只需要一個(gè)圖片的url和一個(gè)占位圖作為參數(shù)而已。而且它的接口方法是以UIImageView的分類的方式提供的,而非通過繼承的方式,這樣降低了耦合和侵入性,同時(shí)也降低了使用者的使用成本,不用在創(chuàng)建UIImageView時(shí)繼承某類,或使用它封裝好的某類。只要在類中導(dǎo)入了該分類的頭文件,就可以通過UIImageView的實(shí)例對(duì)象調(diào)用庫(kù)里接口方法。
UIImageView+WebCache.m
我們從使用者的視角開始,從外向內(nèi)一層層的分析它的代碼。首先我們進(jìn)入摁住command鍵的同時(shí)點(diǎn)擊[_imgV setImageWithURL:url placeholderImage:image];,便進(jìn)入了UIImageView+WebCache.m分類文件。
#import "UIImageView+WebCache.h"
#import "objc/runtime.h"
// 3個(gè)動(dòng)態(tài)添加的屬性
static char imageURLKey; // 圖片url
static char operationKey; // 操作
static char operationArrayKey; // 一組操作
@implementation UIImageView (WebCache)
- (void)setImageWithURL:(NSURL *)url {
[self setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
[self setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options {
[self setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}
- (void)setImageWithURL:(NSURL *)url completed:(SDWebImageCompletedBlock)completedBlock {
[self setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletedBlock)completedBlock {
[self setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletedBlock)completedBlock {
[self setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}
這段代碼值得說的是,所有暴露在外的接口方法內(nèi)部其實(shí)都是在調(diào)用一個(gè)參數(shù)最多的一個(gè)方法,我們暫且將其稱為
全能方法。這樣的話將核心代碼封裝在一處,不必寫冗余餓的代碼,且看起來一目了然。
我們自己在寫一個(gè)自定義控件或者說自己寫東西時(shí),提供的初始化方法和功能方法最好都以此種形式寫。
另外,該分類聲明了三個(gè)靜態(tài)字符,均用于通過運(yùn)行時(shí)動(dòng)態(tài)地給分類添加屬性,也叫屬性關(guān)聯(lián)。分類中是不可以直接定義屬性的,需要通過運(yùn)行時(shí)添加。
接下來我們跳入全能方法觀察它做了什么。首先,我們看到該方法有五個(gè)參數(shù),分別是圖片url,占位圖placeholder,配置選項(xiàng)options,下載進(jìn)行中的進(jìn)度block回調(diào)progressBlock,下載完成后的completedBlock回調(diào)。
- (void)setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletedBlock)completedBlock;
然后,我們開始一步一步的觀察此方法內(nèi)部做了什么:
- (void)setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletedBlock)completedBlock {
[self cancelCurrentImageLoad]; // 取消當(dāng)前的operation(取消操作并設(shè)置operationKey屬性為nil)
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 設(shè)置imageURLKey屬性
self.image = placeholder; // 先設(shè)置占位圖
if (!(options & SDWebImageDelayPlaceholder)) { // SDWebImageDelayPlaceholder枚舉值的含義是取消網(wǎng)絡(luò)圖片加載好前展示占位圖片。所以在這里并不能直接把placeholder直接賦值給self.image,而要用if條件排除這種情況。
self.image = placeholder;
}
if (url) {
// 獲取圖片(先從內(nèi)存找,若無再?gòu)拇疟P找,若仍無則從網(wǎng)絡(luò)下載。)
// 先從內(nèi)存找,若找到,則在主線程設(shè)置圖片,并調(diào)用block回調(diào);若在磁盤找到,則還要將其添加到內(nèi)存緩存再做后續(xù)的;若是從網(wǎng)絡(luò)下載而得,則內(nèi)存和磁盤均要添加緩存,而后再進(jìn)行后續(xù)的事。
__weak UIImageView *wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
if (!wself) return;
dispatch_main_sync_safe(^{ // dispatch_main_sync_safe宏的目的是確保一定在主線程
if (!wself) return;
if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) { // 這個(gè)條件在這里又出現(xiàn)了,SDWebImageDelayPlaceholder的含義是網(wǎng)絡(luò)圖片加載前不顯示占位圖片,但此時(shí)網(wǎng)絡(luò)圖片沒有成功請(qǐng)求到,所以此時(shí)需要加載占位圖。
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType);
}
});
}];
objc_setAssociatedObject(self, &operationKey, operation, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 獲取圖片的同時(shí),將該操作賦為UIImageView+WebCache的屬性。
}
}
該方法內(nèi)部一上來首先就調(diào)用了方法
[self cancelCurrentImageLoad];取消了當(dāng)前UIImageView對(duì)象的獲取圖片操作;然后將參數(shù)url設(shè)為該分類的一個(gè)屬性;接著設(shè)置了UIImageView對(duì)象的占位圖;然后便調(diào)用了SDWebImageManager的實(shí)例對(duì)象方法獲取圖片,在其回調(diào)block里,若獲取圖片成功,則將其設(shè)為UIImageView的圖片,若獲取失敗則將placeholder賦為占位圖,并且最后將完成的block回調(diào)。(盡管我們通常使用的是沒有回調(diào)的接口,若改為有回調(diào)的接口,下載完成后是可以收到回調(diào)的)
SDWebImageManager的獲取圖片的對(duì)象方法是異步下載圖片的,該方法return的是id <SDWebImageOperation>類型的對(duì)象,它代表一個(gè)實(shí)現(xiàn)了SDWebImageOperation協(xié)議的任何對(duì)象,在這里表示獲取圖片這一操作。在調(diào)用了該方法獲取圖片的后,旋即我們便將該方法的返回值,即該operation賦為該分類的屬性。至于為什么要將其賦為屬性?因?yàn)樗慝@取圖片的“操作”,我們要允許后續(xù)將獲取圖片的操作取消。(下面馬上會(huì)講解“取消當(dāng)前獲取圖片的操作”的方法)
objc_setAssociatedObject(self, &operationKey, operation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
上面的代碼有幾處仍需要細(xì)說,或者說留意一下。
First:一開始調(diào)用的取消當(dāng)前獲取圖片的操作那個(gè)方法內(nèi)部是這樣實(shí)現(xiàn)的:
// 取消當(dāng)前UIImageView對(duì)象的獲取圖片操作,并將分類的該屬性賦為nil
- (void)cancelCurrentImageLoad {
// Cancel in progress downloader from queue
id <SDWebImageOperation> operation = objc_getAssociatedObject(self, &operationKey);
if (operation) {
[operation cancel];
objc_setAssociatedObject(self, &operationKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
即通過運(yùn)行時(shí)get到當(dāng)前操作的屬性對(duì)象operation,然后它調(diào)用了[operation cancel];方法,其實(shí)這個(gè)cancel方法是這么回事:獲取圖片的方法返回的是“獲取圖片的操作”——operation對(duì)象,這個(gè)operation是實(shí)現(xiàn)了SDWebImageOperation協(xié)議的一個(gè)對(duì)象。其實(shí)在獲取圖片方法內(nèi)部我們就可以看到(后面將會(huì)說到)該對(duì)象是類SDWebImageCombinedOperation創(chuàng)建的,它正是實(shí)現(xiàn)了SDWebImageOperation協(xié)議,并實(shí)現(xiàn)了cancel方法,在其中進(jìn)行了更多的取消處理(不僅是調(diào)用了NSOperation的cancel方法)。
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
self.cancelBlock = nil;
}
}
** Second:**SDWebImageDelayPlaceholder這個(gè)枚舉值的意思是取消網(wǎng)絡(luò)圖片加載好之前展示占位圖片,即網(wǎng)絡(luò)圖片加載好之前不顯示占位圖。
if (!(options & SDWebImageDelayPlaceholder)) {
self.image = placeholder;
}
在這里并不能直接把placeholder直接賦值給self.image,而要用if條件排除這種情況。然而在網(wǎng)絡(luò)下載圖片的回調(diào)里,若沒有得到圖片的情況下又有段這樣的代碼:
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
此時(shí)網(wǎng)絡(luò)圖片沒有成功請(qǐng)求到,而占位圖此前又沒有設(shè)置,所以此時(shí)單就這種情形需要加載占位圖。
** Third:**在獲取圖片的block回調(diào)里我們可以看到,代碼中使用的是dispatch_main_sync_safe,它是被定義的一個(gè)宏,目的是為了保證是在主線程的。因?yàn)樵O(shè)置圖片這些UI操作,必須得在主線程進(jìn)行。
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
}\
else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}
SDWebImageManager
然后我們點(diǎn)擊downloadWithURL:options: progress: completed:方法便跳入了SDWebImageManager類中。該類是封裝了獲取圖片的整個(gè)動(dòng)作。我們先來看看SDWebImageManager.h頭文件的代碼吧。
@interface SDWebImageManager : NSObject
@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly) SDImageCache *imageCache; // 關(guān)于緩存
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; // 關(guān)于下載
@property (strong) NSString *(^cacheKeyFilter)(NSURL *url);
+ (SDWebImageManager *)sharedManager;
- (id <SDWebImageOperation>)downloadWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletedWithFinishedBlock)completedBlock;
/**
* Saves image to cache for given URL
*
* @param image The image to cache
* @param url The URL to the image
*
*/
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
/**
* Cancel all current opreations
*/
- (void)cancelAll;
/**
* Check one or more operations running
*/
- (BOOL)isRunning;
/**
* Check if image has already been cached
*/
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
- (BOOL)diskImageExistsForURL:(NSURL *)url;
/**
*Return the cache key for a given URL
*/
- (NSString *)cacheKeyForURL:(NSURL *)url;
@end
可以看到屬性中有兩個(gè)是非常重要的:imageCache和imageDownloader分別代表對(duì)緩存的處理和對(duì)下載的處理。同樣下面也定義好幾個(gè)方法,都自帶了注釋。
接下來我們重點(diǎn)看downloadWithURL:options: progress: completed:方法的實(shí)現(xiàn)。代碼太長(zhǎng)就不全部列出了,只列出主要代碼:
- (id <SDWebImageOperation>)downloadWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletedWithFinishedBlock)completedBlock;
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;
}
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; // 該方法return的就是該operation實(shí)例對(duì)象
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url]; // 判斷該url是否已在“黑名單”內(nèi)
}
// 對(duì)于url為空或在黑名單內(nèi)且不允許再次請(qǐng)求配置的情況,則直接在此時(shí)用block回調(diào),并拋出錯(cuò)誤,而不用費(fèi)力做后續(xù)步驟。
if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES);
});
return operation;
}
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation]; // 將當(dāng)前操作,添加進(jìn)runningOperations數(shù)組,表示當(dāng)前正在進(jìn)行的操作們
}
NSString *key = [self cacheKeyForURL:url]; // 轉(zhuǎn)換為url的字符串,圖片在緩存中是以u(píng)rl字符串為key存儲(chǔ)的。
// 先從緩存中查找。返回NSOperation類型的operation對(duì)象
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// If image was found in the cache bug 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.
completedBlock(image, nil, cacheType, YES); // 有圖片就通過block回調(diào)。
});
}
...
...
首先一開始對(duì)參數(shù)的校驗(yàn)及處理,我們自己寫代碼也一定要養(yǎng)成這樣嚴(yán)謹(jǐn)?shù)拇a習(xí)慣。在方法內(nèi)部首先對(duì)參數(shù)進(jìn)行非空或者其他一些校驗(yàn)處理,一來可以防止因?yàn)閰?shù)值為nil引起的崩潰,二來若參數(shù)為nil,則大多數(shù)情況下可以直接
return了,不必再費(fèi)力往下執(zhí)行了。同樣的道理,下面判斷了該url是否在黑名單內(nèi),若在黑名單內(nèi),并且是“不允許再次請(qǐng)求”配置的情況下,就直接在此用block回調(diào),并拋出錯(cuò)誤,而不用費(fèi)力執(zhí)行后續(xù)動(dòng)作。
然后我們看到通過
SDWebImageCombinedOperation類創(chuàng)建了operation對(duì)象,該方法return的就是該對(duì)象,SDWebImageCombinedOperation類就是一個(gè)實(shí)現(xiàn)了SDWebImageOperation協(xié)議的類。
旋即將該
operation對(duì)象加入了self.runningOperations數(shù)組,表示當(dāng)前正在進(jìn)行的操作們。
然后我們以u(píng)rl的字符串為key,調(diào)用
SDImageCache的實(shí)例方法queryDiskCacheForKey:key done:從緩存獲取獲取圖片,若獲取成功則將其回調(diào)。
若從緩存中沒有找到圖片,則繼續(xù)往下執(zhí)行至此,則開始要從網(wǎng)絡(luò)請(qǐng)求圖片了。
// 執(zhí)行到此,說明緩存中并未找到圖片,所以得從網(wǎng)絡(luò)下載。
// 該方法進(jìn)行異步下載圖片,并返回了一個(gè)subOperation對(duì)象,它是一個(gè)實(shí)現(xiàn)了SDWebImageOperation協(xié)議的對(duì)象
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url
options:downloaderOptions
progress:progressBlock
completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
...
...
...
}];
然后在completed回調(diào)里,若下載成功,則首先將該圖片存入緩存,然后回調(diào)至主線程。
// 下載成功后,將圖片添加到緩存中,并通過block回調(diào)。
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished);
});
并且,若該下載操作已完成,則runningOperations數(shù)組中移除該operation:
if (finished) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
并且實(shí)現(xiàn)了operation的cancelBlockblock回調(diào):
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:weakOperation];
}
};
結(jié)尾
SDWebImage的整體脈絡(luò)結(jié)構(gòu),及它加載圖片的思路就是以上了,看那張圖更簡(jiǎn)潔清楚。但是這些也只是大體的脈絡(luò)而已,還有最重要的緩存部分,以及網(wǎng)絡(luò)請(qǐng)求部分都還沒寫,留在后續(xù),分別另開兩篇研究其緩存和網(wǎng)絡(luò)部分。