【原】SDWebImage源碼閱讀(二)


【原】SDWebImage源碼閱讀(二)

本文轉載請注明出處 —— polobymulberry-博客園

1. 解決上一篇遺留的坑


上一篇中對sd_setImageWithURL函數簡單分析了一下,還留了一些坑。不過因為我們現在對這個函數有一個大概框架了,我們就按順序一個個來解決。

首先是這一句代碼:

objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

就是給UIImageView當前這個對象添加一個NSString的關聯對象url。相當於現在這個圖片的url屬性綁定到了UIImageView對象上。如果對這個函數有疑問,請移步我的這篇博客

下面簡單的部分我就不說了,直接跳到

if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
    completedBlock(image, error, cacheType, url);
    return;
}

首先是SDWebImageAvoidAutoSetImage,我們看看它的注釋

/**
 * By default, image is added to the imageView after download. But in some cases, we want to
 * have the hand before setting the image (apply a filter or add it with cross-fade animation for instance)
 * Use this flag if you want to manually set the image in the completion when success
 */

翻譯過來就是說,默認情況下是等image完全從網絡端下載完后,就會直接將結果設置到UIImageView。但是有些人想在獲取到圖片后,對圖片做一些處理,比如使用filter去渲染圖片或者給圖片加個cross-fade animation(淡出動畫)顯示出來。那你就設置這個選項。然后得手動去處理圖片下載完成后的事情。

上面說了要手動處理了,很自然你就會想到,這個手動處理就是compeletedBlock啊!當然,除了有這個枚舉選項時需要手動處理,其實只要你自定義了compeletedBlock,都會調用你自定義處理的函數。你說我怎么知道的?你看下面的代碼,如果你自定義了下載完成后的處理方式,並且也確實下載完成了(finished為YES),就執行自定義方式:

if (completedBlock && finished) {
    completedBlock(image, error, cacheType, url);
}

最后還剩下一個情況,就是url不存在的情況(注意上面講的是if(url){…},下面講的是在else{…}):

dispatch_main_async_safe(^{
    [self removeActivityIndicator];
    NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
    if (completedBlock) {
        completedBlock(nil, error, SDImageCacheTypeNone, url);
    }
});

首先自定義一個NSError的對象,表示url為空的錯誤。然后傳給compeletedBlock。


知識點:NSError構造方法errorWithDomain

+ (instancetype)errorWithDomain:(NSString *)domain code:(NSInteger)code userInfo:(nullable NSDictionary *)dict;

具體細節可以移步我的這篇博客


其實目前來說,我心中還有兩個最大的疑惑,一個就是operation怎么執行的一個就是如何自定義compeletedBlock

2. operation執行過程

我們可以看到這里downloadImageWithURL其實是SDWebImageManager的方法

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

我們進去該函數實現,快兩百行的代碼了。好吧,先歇着,我們看看注釋(我直接貼出我翻譯過后的注釋)。

/**
 * 如果圖片不在緩存中,根據指定的URL下載圖片,否則使用緩存中的圖片。.
 *
 * @param url            圖片的URL
 * @param options        該請求所要遵循的選項。(前面已經介紹了兩個)
 * @param progressBlock  當圖片正在下載時調用該block。
 * @param completedBlock 當操作完成后調用該block。
 *
 *   該參數是必須的。(指的是completedBlock)
 * 
 *   該block沒有返回值並且用請求的UIImage作為第一個參數。
 *   如果請求出錯,那么image參數為nil,而第二參數將包含一個NSError對象。
 *
 *   第三個參數是'SDImageCacheType'枚舉,表明該圖片重新獲取方式是從本地緩存(硬盤)或者
 *   從內存緩存,還是從網絡端重新獲取image一遍。.
 *
 *   當options設為SDWebImageProgressiveDownload並且此時圖片正在下載,finished將設為NO
 *   因此這個block會不停地調用直到圖片下載完成,此時才會設置finished為YES.
 *
 * @return 返回一個遵循SDWebImageOperation協議的NSObject. 應該是一個SDWebImageDownloaderOperation的實例
 */

上面的注釋有幾個不認識的概念。一個是SDImageCacheType,另一個是options中的SDWebImageProgressiveDownload,還有一個SDWebImageDownloaderOperation

2.1 SDImageCacheType


typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
     * 該圖片無法從SDWebImage的緩存中獲取,必須從web端下載。
     */
    SDImageCacheTypeNone,
    /**
     * 圖片從硬盤緩存(disk cache)中獲取
     */
    SDImageCacheTypeDisk,
    /**
     * 圖片從硬盤緩存(disk cache)中獲取
     */
    SDImageCacheTypeMemory
};

具體緩存實現方式我放在SDWebImage源碼閱讀(五)了。

2.2 SDWebImageProgressiveDownload


如果在加載圖片中設定了該選項,那么圖片會隨着下載的進度一點點地顯示出來。缺省情況下,圖片是下載完成后一次顯示出來的。

2.3 SDWebImageDownloaderOperation


看到這個類,我內心是愉快的。之前我不是說這個opertion應該和NSOperation有些關系嗎?這個類就是NSOperation的子類啊,並且遵循SDWebImageOperation協議。這下SDWebImageDownloaderOperation將NSOperation和SDWebImageOperation聯系在了一起,我們可以看下它的聲明:

@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageOperation>

所以不用說,這個類一定是個重頭戲。

但是我們搜索SDWebImageDownloaderOperation,發現SDWebImageManager中的downloadImageWithURL函數並沒有返回SDWebImageDownloaderOperation。這一點很讓人疑惑。不過我們發現SDWebImageDownloaderOperation遵循SDWebImageOperation協議,會不會和downloadImageWithURL的返回值id<SDWebImageOperation>有關系?而且看到這,我有些迷糊了。函數返回值為id<protocol>,這是什么返回值?什么時候需要這樣用?感覺源碼閱讀進行不下去了。。。先不急,既然注釋說返回的是SDWebImageDownloaderOperation,那么肯定就是啦。

我先在所有工程中搜索SDWebImageDownloaderOperation,發現在SDWebImageDownloader中也有一個downloadImageWithURL函數。而且里面就定義了一個opertion,這個opertion就是SDWebImageDownloaderOperation 類型,並且這個函數也是返回operation的。

__block SDWebImageDownloaderOperation *operation;

回到我們的SDWebImageManager中的downloadImageWithURL函數中,搜索downloadImageWithURL,找找看是不是有蛛絲馬跡。果然,下面這段代碼:

id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)

此處的downloadImageWithURL是SDWebImageDownloader的一個方法。

好,但此為止,我們也只是覺得上面那些東西有聯系,但是聯系並不是很清晰。而且已經有了一個downloadImageWithURL,還要弄一個干什么?

太多問題了,我現在也只能大概猜測到底怎么回事了,在繼續探索之前,我們先整理這里面的關系:

QQ20151226-0@2x

上面文字部分,我用紅色標注了兩個問題,我們先解決第一個


問題一:函數返回值為id<protocol>,這是什么返回值?

其實返回的是一個id類型,只是這個id類型一定要遵循里面的protocol,比如id<SDWebImageOperation>,那么因為SDWebImageDownloaderOperation遵循SDWebImageOperation協議,所以可以作為返回類型。



問題二:已經有了一個downloadImageWithURL,還要弄一個干什么?

這個說實話,我也不是很清楚,只能找這兩個函數之間的關聯了。其實更准確地說是找SDWebImageManager中downloadImageWithURL中的subOperation(SDWebImageDownloaderOperation)和operation的關系。根據這個思路,我發現了subOperation只在這個函數里面出現了兩次。第一次是定義的地方,第二次就是:

operation.cancelBlock = ^{
    [subOperation cancel];
                
    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:weakOperation];
    }
};

無語,你辛辛苦苦弄了個subOperation,結果就亮了個像,還是cancel,就沒了。太沒人性了。不過是細想其實是有原因的,我在SDWebImageDownloaderOperation的downloadImageWithURL函數注釋中找到了答案:

@return A cancellable SDWebImageOperation

一個cancellable的SDWebImageOperation,是不是和這里只用了cancel對應上了。雖然找到了點聯系,不過還是流於表面,這與為什么這么做,這么做的理由還不是很清楚。

我們還是細細分析cancelBlock那段代碼

首先是一個block,operation.cancelBlock有一個對應的setCancelBlock函數:

- (void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
    // 檢測self(是一個SDWebImageCombinedOperation類型的operation)是否取消了,如果取消了,就執行對應的cancelBlock函數。
    if (self.isCancelled) {
        if (cancelBlock) {
            cancelBlock();
        }
        _cancelBlock = nil; //不要忘了置cancelBlock為nil,否則會crash
    } else {
        _cancelBlock = [cancelBlock copy];
    }
}

也就是說operation如果取消了,那么就會執行subOperation的cancel函數。並且從runningOperations中移除該operation,因為是block,為了避免循環引用,所以使用了weakOperation。runningOperations大概從名字也能猜到,用來存儲正在運行的operation。既然當前operation被取消了,肯定要從runningOperations移除的嘛!

注意此處的operation的類型是SDWebImageCombinedOperation,具體定義如下:

// 遵循SDWebImageOperation
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
// 注意SDWebImageCombinedOperation遵循SDWebImageOperation,所以實現了cancel方法
// 在cancel方法中,主要是調用了cancelBlock,這個設計很值得琢磨
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
// 根據cacheType獲取到image,這里雖然名字用的是cache,但是如果cache沒有獲取到圖片
// 還是要把image下載下來的。此處只是把通過cache獲取image和通過download獲取image封裝起來
@property (strong, nonatomic) NSOperation *cacheOperation;

@end

還有一個問題就是@synchronized是什么?

知識點:@synchronized

避免多個線程執行同一段代碼,主要防止當前operation會被多次remove,從而造成crash。這里括號內的self.runningOperations是用作互斥信號量。 即此時其他線程不能修改self.runningOperations中的屬性。


雖然看懂了這段代碼,可是后面不知道該看什么了。

所以我還是從頭看這段代碼(SDWebImageManager中downloadImageWithURL),看能不能找到點什么頭緒:

// 如果調用此方法,而沒有傳completedBlock,那將是無意義的
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

不要將completedBlock參數設為nil,因為這樣做是毫無意義的。如果你是想使用downloadImageWithURL來預先獲取image,那就應該使用[SDWebImagePrefetcher prefetchURLs],而不是直接調用SDWebImageManager中的downloadImageWithURL函數。

// 使用NSString對象而非NSURL作為url是常見的錯誤. 因為某些奇怪的原因,Xcode不會報任何類型不匹配的警告,這里允許傳NSString對象給URL。
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // 防止傳了一個NSNull值給NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

我覺得此處對於細節地處理很值得學習。

__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;

這個沒啥好說的,避免循環引用,使用了weak。

    BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }

    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從字面上理解就是一組下載失敗的圖片URL。所以這段代碼也很好理解,就是如果這個圖片url無法下載,那就使用completedBlock進行錯誤處理。那什么情況下算這個圖片url無法下載呢?第一種情況是該url為空,另一種情況就是如果是failedUrl也無法下載,但是要避免無法下載就放入failedUrl的情況,就要設置options為SDWebImageRetryFailed。一般默認image無法下載,這個url就會加入黑名單,但是設置了SDWebImageRetryFailed會禁止添加到黑名單,不停重新下載。

如果該url可以下載,那么就添加一個新的operation到runningOperations中。

@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

剩下的100多行就是為了生成一個cacheOperation。那它到底是何方神聖?它是一個NSOperation,所以加入NSOperationQueue會自動執行。不過我還是全局搜索cacheOperation,發現它在SDWebImageCombinedOperation中的cancel方法中調用了:

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        
        // TODO: this is a temporary fix to #809.
        // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
//        self.cancelBlock = nil;
        _cancelBlock = nil;
    }
}

還記得SDWebImageCombinedOperation遵循SDWebImageOperation協議嗎?這就是SDWebImage實現的cancel。而cacheOperation是NSOperation,所以調用自身的cancel。注意是在這才會設置cancelled設為YES。

好,現在回來看這個queryDiskCacheForKey函數。在此之前,先看上面有段代碼,用圖片的url來獲取cache對應的key,也就是說cache中如果已經有了該圖片,那就返回該圖片在cache中對應的key,你可以根據這個key去cache中獲取圖片。

NSString *key = [self cacheKeyForURL:url];

獲取到key后,你就可以使用queryDiskCacheForKey函數去查找了:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    // 如果doneBlock不存在。那么就return nil。這個處理和downloadImageWithURL的completedBlock很類似
    if (!doneBlock) {
        return nil;
    }
    // 如果key為nil,說明cache中沒有該image。所以doneBlock中傳入SDImageCacheTypeNone,表示cache中沒有圖片,要從網絡重新獲取。
    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }
   // 如果key不為nil
    // 首先在內存cache中查找    
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    // 找到了,就傳入SDImageCacheTypeMemory,說是在內存cache中獲取的
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    // 否則,說明圖片就在磁盤cache中。
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            UIImage *diskImage = [self diskImageForKey:key];
            // 如果磁盤中得到了該image,並且還需要緩存到內存中,為了同步最新數據
            if (diskImage && self.shouldCacheImagesInMemory) {
                // 后面細講
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            // 傳入SDImageCacheTypeDisk,說明是從磁盤中獲取的
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

其實這段代碼如果不深究的話,也很容易理解的。我直接把說明寫成注釋在上面了。不過這里我還有個疑問,就是為啥operation要在查找硬盤緩存時,才創建了一個新的operation?這里我談談我的想法:因為“圖片可以用”這個狀態意味着圖片必須在內存中了。圖片在網絡還是在硬盤,其實相對來說並沒有本質區別,最后都是要加進內存的。所以這里就有一個加載到內存的過程,需要產生一個NSOperation,也就理所當然會發生cancel。我這也不是胡亂猜的,函數中有一個self.ioQueue。表明這是一個io序列(dispatch_queue_t)。

這下再回到downloadImageWithURL里面剩下的代碼,就會很輕松了。因為它無非就是要處理上面那幾種cache情況嘛。

我們還是一點點來看done^{}中的代碼:

if (operation.isCancelled) {
    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:operation];
    }

    return;
}

這段代碼簡單,不解釋了,隨時判斷該operation是否已經cancel了。

下面又是一段巨長的代碼,我們先看看if中表示什么:

if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
    // ...
}

我們先看后面那個delegate方法:

/**
 * 當image無法在緩存中找到,調用該函數控制該image的下載
 *
 * @param imageManager 當前的`SDWebImageManager`
 * @param imageURL     需要下載的image的URL
 *
 * @return 返回NO表示當圖片緩存未命中,反而阻止圖片下載。如果該函數沒實現,相當於返回YES。
 */
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

這里要着重說明下此處return的含義。

注意在if里面最后一組||表達式,使用了短路判斷(A||B,只要A為真,就不用判斷B了),也就是說,如果delegate沒有實現上面那個函數,整個表達式就為真,相當於該函數返回了YES。如果delegate實現了該函數,那就執行該函數,並且判斷該函數執行結果。如果函數返回NO,那么整個if表達式都為NO,那么當圖片緩存未命中時,圖片下載反而被阻止。

目前我看的源碼中並沒有地方實現了該函數,所以就當if后半段恆為YES。我們主要還是看前面那個||表達式:

(!image || options & SDWebImageRefreshCached)

如果沒有緩存到image,或者options中有SDWebImageRefreshCached選項,就執行if語句。現在我們深入看看if判斷下的代碼到底執行了什么,首先又是一個if語句:

if (image && options & SDWebImageRefreshCached) {
    dispatch_main_sync_safe(^{
        // 如果圖片在緩存中找到,但是options中有SDWebImageRefreshCached
// 那么就嘗試重新下載該圖片,這樣是NSURLCache有機會從服務器端刷新自身緩存。
        completedBlock(image, nil, cacheType, YES, url);
    });
}

下面的代碼就表示開始要下載圖片了。

首先定義了一個SDWebImageDownloaderOptions枚舉值downloaderOptions,並根據options來設置downloaderOptions。基本上SDWebImageOptions和SDWebImageDownloaderOptions是一一對應的。只需要注意最后一個選項SDWebImageRefreshCached,這個得先強制關閉ProgressiveDownload方式。那后面的SDWebImageDownloaderIgnoreCachedResponse是什么意思呢?可能會有這樣的疑惑,不是已經從imageCache中獲取到了image了嗎?還要Ignore干啥?這里簡單提下,后面會詳解:因為SDWebImage有兩種緩存方式,一個是SDImageCache,一個就是NSURLCache,所以知道為什么這個選項是Ignore了吧,因為已經從SDImageCache獲取了image,就忽略NSURLCache了。

if (image && options & SDWebImageRefreshCached) {
        // 相當於downloaderOptions =  downloaderOption & ~SDWebImageDownloaderProgressiveDownload);
        downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
        // 相當於 downloaderOptions = (downloaderOptions | SDWebImageDownloaderIgnoreCachedResponse);
        downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}

然后生成了一個subOperation,這段代碼也很長,我大致看了下SDWebImageDownloader中的downloadImageWithURL函數,感覺終於到了 “真正”下載的代碼了。為什么這么說了?因為里面代碼大部分都是iOS自帶框架底層的代碼了。總算到頭了。不過這段代碼我准備下一篇再看。

直接跳出這個subOperation的賦值語句,來到對應的else if語句:

else if (image) {
       // 從緩存中獲取到了圖片,而且不需要刷新緩存的
 // 直接執行completedBlock,其中error置為nil即可。
        dispatch_main_sync_safe(^{
            if (!weakOperation.isCancelled) {
                completedBlock(image, nil, cacheType, YES, url);
            }
        });
        // 執行完后,說明圖片獲取成功,可以把當前這個operation溢移除了。
        @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:operation];
        }
    }
    else {
        // 又沒有從緩存中獲取到圖片,shouldDownloadImageForURL又返回NO,不允許下載,悲催!
        // 所以completedBlock中image和error均傳入nil。 
        dispatch_main_sync_safe(^{
            if (!weakOperation.isCancelled) {
                completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
            }
        });
        @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:operation];
        }
 }

恩,SDWebImageManager中的downloadImageWithURL函數我們還剩下那個最精彩的SDWebImageDownloader中的downloadImageWithURL函數,留着下一篇閱讀。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM