SDWebImage源碼閱讀1——整體脈絡(luò)結(jié)構(gòu)

前言

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ì)照該圖加深理解。

屏幕快照 2016-08-01 下午2.31.38.png

我們一般在在項(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)用了NSOperationcancel方法)。

- (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è)是非常重要的:imageCacheimageDownloader分別代表對(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)了operationcancelBlockblock回調(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ò)部分。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容