閱讀源代碼:SDWebImage 3.8.2

簡單的介紹

SDWebImage 提供圖片的異步下載和緩存,對外通過 categories 封裝 UIImageViewUIButton,MKAnnotationView 的接口供使用者使用。
SDWebImage 的磁盤緩存有對有效期和最大容量的限制處理,內(nèi)存緩存在系統(tǒng)報(bào)內(nèi)存警告的時(shí)候會清除。它還保障幾個(gè)相同的 URL 不會被重復(fù)下載,不可用的 URL 不會一次次的被重試,主線程絕不會被阻塞。

直觀的感受

SDWebImage 的接口簡單易用,開發(fā)者一句代碼就能使用,代碼: - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; ,復(fù)雜的邏輯和實(shí)現(xiàn)都被隱藏在這句代碼的后面。
各模塊獨(dú)立,Cache緩存模塊提供庫的內(nèi)存緩存和可選的磁盤緩存支持,Downloader下載模塊提供基于 NSURLSessionNSOperation 的下載器。SDWebImageManager 則將 Cache 和 Downloader 兩個(gè)模塊很好的整合在一起,提供基于緩存的圖片加載功能。最后是使用 categories 封裝了常用 UI 控件的接口。
GitHub 上擁有萬的 star,無數(shù)的使用者,在項(xiàng)目中久經(jīng)考驗(yàn),是程序猿學(xué)習(xí)的好材料。

從‘頭’開始

這個(gè)頭就是最常用的類 UIImageView+WebCache ,類里面暴露了很多加載圖片的接口:

- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
...

但是歸根到底都是調(diào)用的這句,我們來看這里面的代碼。

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

代碼的第一句是

[self sd_cancelCurrentImageLoad];

看方法名就知道它的作用,就是取消這個(gè)視圖 ImageView 正在加載圖片的操作,如果這個(gè) ImageView 正在加載圖片,保障在開始新的加載圖片任務(wù)之前,取消掉正在進(jìn)行的加載操作。

看下具體的實(shí)現(xiàn)代碼 UIImageView+WebCache

- (void)sd_cancelCurrentImageLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
- (void)sd_cancelCurrentAnimationImagesLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewAnimationImages"];
}

兩個(gè) key 說明有兩個(gè)不一樣的加載方式,一個(gè)是單張圖片的,另一個(gè)是連續(xù)下載多張,放到 NSArray<UIImage *> *animationImages 中。

看下取消操作的代碼實(shí)現(xiàn),UIView (WebCacheOperation):

- (NSMutableDictionary *)operationDictionary {
    NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    if (operations) {
        return operations;
    }
    operations = [NSMutableDictionary dictionary];
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
    [self sd_cancelImageLoadOperationWithKey:key];
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary setObject:operation forKey:key];
}
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    // 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];
    }
}

代碼中,通過 objc_setAssociatedObject 關(guān)聯(lián)對象的方法,給 UIImageView 動態(tài)添加了一個(gè) NSMutableDictionary 的屬性。通過 key-value 維護(hù)這個(gè) ImageView 已經(jīng)有了哪些下載操作,如果是數(shù)組就是 UIImageViewAnimationImages 否則就是 UIImageViewImageLoad 。最后獲得的都是遵從了 <SDWebImageOperation> 協(xié)議的對象,可以統(tǒng)一調(diào)用定義好的方法 cancel,達(dá)到取消下載操作的目的,如果 operation 都被取消了,則刪除對應(yīng) key 的值。

繼續(xù)看 - (void)sd_setImageWithURL: placeholderImage: options: ; 里的代碼

if (!(options & SDWebImageDelayPlaceholder)) {
    dispatch_main_async_safe(^{
        self.image = placeholder;
    });
}

如果加載圖片的選項(xiàng)不是 SDWebImageDelayPlaceholder 則會在主線程中先設(shè)置 placeholder 的占位圖,

SDWebImageDelayPlaceholder 的情況后面說。dispatch_main_async_safe 是一個(gè)宏定義,我們可以參考這種寫法。

#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }

看起來很簡潔。

下面這段就是這個(gè)類中比較關(guān)鍵的代碼了,

// check if activityView is enabled or not
if ([self showActivityIndicatorView]) {
    [self addActivityIndicator];
}

__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
    [wself removeActivityIndicator];
    if (!wself) return;
    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 {
            if ((options & SDWebImageDelayPlaceholder)) {
                wself.image = placeholder;
                [wself setNeedsLayout];
            }
        }
        if (completedBlock && finished) {
            completedBlock(image, error, cacheType, url);
        }
    });
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

先檢查 activityView 是否可用,可用的話給 ImageView 正中間添加一個(gè)活動指示器,并旋轉(zhuǎn),加載圖片完成或失敗都會清除掉。__weak __typeof(self)wself = self; 避免循環(huán)引用,接下來就是調(diào)用 SDWebImageManager 的方法 downloadImageWithURL: options: progress: completed: ,在該方法的 completed block 回調(diào)中,如果 option 是 SDWebImageAvoidAutoSetImage ,就是要求不要給 ImageView 自動設(shè)置圖片,則只回調(diào) completedBlock 然后 return,否則有 image 就設(shè)置給 ImageView 。沒有 image 通常就是錯誤情況,如果 option 是 SDWebImageDelayPlaceholder 則設(shè)置占位圖(可以設(shè)置成提示用戶圖片沒加載出來的圖片),最后回調(diào) completedBlock。上面代碼最后一句是把這個(gè) operation 存到 ImageView 的 NSMutableDictionary 中,為了之前提到的 [self sd_cancelCurrentImageLoad]; 操作準(zhǔn)備的。

UIImageView+WebCache 類中的代碼還是很容易的,邏輯很清晰,沒有難懂的地方。接下來我們?nèi)タ?SDWebImageManager 的核心方法:

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

Cache 和 Downloader 的管理者 SDWebImageManager

我們看這個(gè)方法前幾句:

// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
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 做為參數(shù)傳進(jìn)來是很常見的錯誤,但奇怪的是 XCode 沒有類型錯誤的警告。這個(gè)我試過是有警告提示的,可能更早一些的 XCode 版本是這樣的。這幾句就是參數(shù)的校驗(yàn),沒什么好說的。

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

這個(gè)類的作用是管理多個(gè)模塊的取消操作,具體是怎么實(shí)現(xiàn)的,后面的代碼會提到,__weak 修飾是為了防止循環(huán)引用。

下面就是文章最開始提到的功能之一,不可用的 URL 不會一次次重試的功能實(shí)現(xiàn):

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;
}

在 downloader 的下載方法的 completedBlock 中會將下載失敗的 URL ,維護(hù)到 Set 集合中(黑名單),代碼會在后面提到。這段代碼的意思是如果發(fā)現(xiàn) url 長度為 0 ,或者是下載失敗過的 url ,且沒有要求重試則直接創(chuàng)建 NSError 并 回調(diào) completedBlock 。

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

manager 維護(hù)了一個(gè)數(shù)組 self.runningOperations ,將所有操作放進(jìn)去,便于管理。(比如統(tǒng)一調(diào)用 cancel )

下面是比較核心的代碼:

NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {...}

通過 url 獲取用來緩存的 key,嘗試去緩存中取圖片。queryDiskCacheForKey: done: 方法返回了一個(gè) NSOperation 對象并賦值給了 SDWebImageCombinedOperationcacheOperation ,這個(gè)類(SDWebImageCombinedOperation)中還有一個(gè)屬性 cancelBlock 也會包括一些取消操作。它還實(shí)現(xiàn)了協(xié)議 <SDWebImageOperation> ,這個(gè)協(xié)議里只需要實(shí)現(xiàn)一個(gè)方法,就是 - (void)cancel; 。

@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end

SDWebImageCombinedOperation 的實(shí)現(xiàn)類中,實(shí)現(xiàn)了這個(gè) cancel 方法,并調(diào)用了這些取消操作。

- (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;
    }
}

這樣,調(diào)用者只要調(diào)用 operation 的 cancel() ,就可以統(tǒng)一對多個(gè)模塊類做取消操作。

然后看下查詢緩存的方法 - (NSOperation *)queryDiskCacheForKey: done: 的實(shí)現(xiàn)代碼:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

會先到內(nèi)存緩存中去查找,如果命中則直接回調(diào),沒有命中繼續(xù)在磁盤緩存中查找。查找任務(wù)是異步的,在一個(gè)串行隊(duì)列中,先生成一個(gè) NSOperation 對象返回,也就是賦值給了 operation.cacheOperation 。在查找任務(wù)中,會先檢測這個(gè) operation 有沒有被取消,如果取消則直接 return,這就實(shí)現(xiàn)了 SDWebImageCombinedOperation 可以取消查找緩存的操作。之后的代碼就是去磁盤緩存中查找圖片,且如果需要內(nèi)存緩存就存進(jìn)去,最后回調(diào) doneBlock。這段代碼被放入到了 @autoreleasepool 中包裹起來,是因?yàn)椴檎页鰜淼膱D片可能會比較大,占用較多的內(nèi)存,保障能夠及時(shí)的回收它。關(guān)于 @releasepool 的原理參考這篇文章。

現(xiàn)在回到 SDWebImageManager 中繼續(xù)看,在 queryDiskCacheForKey: 的 doneBlock 中,

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

如果操作被取消,則刪除掉 self.runningOperations 的操作,然后 return。

接下來會有三個(gè)條件分支,我們一個(gè)個(gè)來看,第一個(gè)是:

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

image 為空意味著緩存沒有命中,SDWebImageRefreshCached 則是就算緩存命中也要下載圖片更新緩存,SDWebImageManager 這個(gè)類還定義了一個(gè)協(xié)議并實(shí)現(xiàn)一個(gè)代理。

@protocol SDWebImageManagerDelegate <NSObject>
@optional
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
@end

第一個(gè)方法的意思是,是否下載圖片,YES 就是下載,NO 就是不下載。第二個(gè)方法是,對下載好的圖片 image 做 transform 處理,比如可以改成圓角圖片等,然后返回,這樣緩存的圖片也會是 transform 之后的圖片。
理解了代理方法的意思就可以理解這個(gè)條件了,如果緩存沒有命中,或需要刷新已有緩存 且 沒有實(shí)現(xiàn) imageManager:shouldDownloadImageForURL 的方法(默認(rèn)是 YES,可以下載圖片)則去下載圖片。如果實(shí)現(xiàn)了這個(gè)代理方法返回的是 YES,也會去下載圖片。

看第一個(gè)條件里的代碼,首先:

if (image && options & SDWebImageRefreshCached) {
    dispatch_main_sync_safe(^{
        // If image was found in the cache but 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, url);
    });
}

緩存如果命中,且需要更新緩存,則先將緩存圖片通過 completedBlock 回調(diào)出去,在繼續(xù)下載圖片。

在往下就是緩存沒有命中或需要更新緩存的情況,所以需要下載圖片,但之前先將 SDWebImageManager 里 option 的條件映射成 SDWebImageDownloder 里的 option 的條件,下載使用的方法是 SDWebImageDownloder 里的 - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock; 方法。具體實(shí)現(xiàn)我們在分析 SDWebImageDownloder 時(shí)會說,先看 completedBlock 里的邏輯。

__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
    // Do nothing if the operation was cancelled
    // See #699 for more details
    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
}

這里注意避免循環(huán)引用,imageDownloaderSDWebImageManager 強(qiáng)引用,downloadImageWithURL 的 completedBlock 會被 imageDownloader 的屬性 URLCallbacks 數(shù)組強(qiáng)引用保存起來,至于為什么這么做后面會講到。

然后是發(fā)生錯誤的處理情況:

else if (error) {
    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];
        }
    }
}

發(fā)生錯誤,并回調(diào)。將在確定條件下失敗的 URL 放入黑名單,不會反復(fù)請求。(通常是 URL 的問題,而不是網(wǎng)絡(luò)問題)

看下 else 之后的代碼,稍長一些:

else {
    if ((options & SDWebImageRetryFailed)) {
        @synchronized (self.failedURLs) {
            [self.failedURLs removeObject:url];
        }
    }
    
    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
        // Image refresh hit the NSURLCache cache, do not call the completion block
    }
    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
        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);
                }
            });
        });
    }
    else {
        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);
            }
        });
    }
}

如果 option 是 SDWebImageRetryFailed 則這個(gè) url 從黑名單中刪除,給它一個(gè)重試的機(jī)會。有 downloadedImage 說明圖片下載成功,如果是要進(jìn)行 transform ,則調(diào)用 delegate 方法,獲取 transform 之后的圖片,進(jìn)行緩存,再調(diào)用 completedBlock 。如果不需要 transform 則直接緩存后回調(diào) completedBlock。

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

這是下載圖片的取消操作,調(diào)用 NSOperation 的 cancel,從 self.runningOperations 中刪除 operation。賦值給 cancelBlock ,交給 SDWebImageCombinedOperation 對象管理。

看前面提到的三個(gè)分支條件的第二個(gè):

else if (image) {
    dispatch_main_sync_safe(^{
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        if (strongOperation && !strongOperation.isCancelled) {
            completedBlock(image, nil, cacheType, YES, url);
        }
    });
    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:operation];
    }
}

有 image 說明緩存命中,且沒有要重下圖片的情況,則直接回調(diào) completedBlock 就可以了。

第三個(gè)分支條件,它的意思是既沒有緩存圖片,代理 delegate 也不允許下載圖片,那就只能直接回調(diào) completedBlock ,圖片參數(shù)傳 nil 了。

else {
    // Image not in cache and download disallowed by delegate
    dispatch_main_sync_safe(^{
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        if (strongOperation && !weakOperation.isCancelled) {
            completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
        }
    });
    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:operation];
    }
}

到此,SDWebImageManager 的核心方法:

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

就介紹完了。

圖片下載器 SDWebImageDownloader

我們還是從 SDWebImageDownloader 的核心方法入手:

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

該方法的大部分代碼都放到了一個(gè) createCallback 的回調(diào)中:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak __typeof(self)wself = self;

    [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
        ...
    }];

    return operation;
}

那我們只能先去探究下 - (void)addProgressCallback: completedBlock: forURL: createCallback: ; 這個(gè)方法,它的實(shí)現(xiàn)代碼如下:

// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) {
    if (completedBlock != nil) {
        completedBlock(nil, nil, nil, NO);
    }
    return;
}

dispatch_barrier_sync(self.barrierQueue, ^{
    BOOL first = NO;
    if (!self.URLCallbacks[url]) {
        self.URLCallbacks[url] = [NSMutableArray new];
        first = YES;
    }

    // Handle single download of simultaneous download request for the same URL
    NSMutableArray *callbacksForURL = self.URLCallbacks[url];
    NSMutableDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    [callbacksForURL addObject:callbacks];
    self.URLCallbacks[url] = callbacksForURL;

    if (first) {
        createCallback();
    }
});

注釋有說,這個(gè) URL 參數(shù)不能為空,因?yàn)樗鳛榇鎯?callbacks 字典的 key,如果它為 nil 則會馬上調(diào)用 completed block 返回 nil 圖片和 nil 數(shù)據(jù)。
self.URLCallbacks 是一個(gè) NSMutableDictionary ,它以 URL 作為 key ,維護(hù)一個(gè)可變數(shù)組 callbacksForURL,這個(gè)數(shù)組里又會存放一個(gè)一個(gè)的 NSMutableDictionary 用來存儲兩個(gè) callback 回調(diào)方法,分別是以 kProgressCallbackKey 為 key 的 progressBlock 和 以 kCompletedCallbackKey 為 key 的 completedBlock 。代碼里還有一個(gè) BOOL first 的變量,如果發(fā)現(xiàn) self.URLCallbacks 中沒有這個(gè) URL 的回調(diào)數(shù)組,那這個(gè) URL 此時(shí)就是第一次請求(此時(shí)沒有相同的 URL 在請求),會調(diào)用 createCallback(); 來創(chuàng)建下載的操作,而發(fā)現(xiàn) self.URLCallbacks 中有這個(gè) URL 的回調(diào)數(shù)組的話,則將對應(yīng)的那兩個(gè)回調(diào)方法存進(jìn) NSMutableDictionary ,在放到之前的回調(diào)數(shù)組中,且不會再調(diào)用 createCallback(); ,這樣相同的 URL 不會重復(fù)請求下載。當(dāng)?shù)谝粋€(gè)請求下載成功之后,會遍歷這個(gè)回調(diào)數(shù)組,將數(shù)組里所有的 callback 都執(zhí)行一遍。這么做的目的就是防止同時(shí)有多個(gè)相同 URL 的請求發(fā)生。
這段代碼使用 dispatch_barrier_sync 將任務(wù)放入一個(gè)并發(fā)隊(duì)列,目的是在并發(fā)隊(duì)列中,這個(gè)任務(wù)執(zhí)行時(shí),不允許別的任務(wù)同時(shí)執(zhí)行。因?yàn)?downloadImageWithURL: 方法要返回一個(gè)遵從<SDWebImageOperation> 的對象,所以要同步執(zhí)行而不能異步。

現(xiàn)在就可以看一看 createCallback(); 中到底都做了什么。

NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
    timeoutInterval = 15.0;
}

// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
    request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
    request.allHTTPHeaderFields = wself.HTTPHeaders;
}
operation = [[wself.operationClass alloc] initWithRequest:request
                                                inSession:self.session
                                                  options:options
                                                 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                     SDWebImageDownloader *sself = wself;
                                                     if (!sself) return;
                                                     __block NSArray *callbacksForURL;
                                                     dispatch_sync(sself.barrierQueue, ^{
                                                         callbacksForURL = [sself.URLCallbacks[url] copy];
                                                     });
                                                     for (NSDictionary *callbacks in callbacksForURL) {
                                                         dispatch_async(dispatch_get_main_queue(), ^{
                                                             SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                             if (callback) callback(receivedSize, expectedSize);
                                                         });
                                                     }
                                                 }
                                                completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                                    SDWebImageDownloader *sself = wself;
                                                    if (!sself) return;
                                                    __block NSArray *callbacksForURL;
                                                    dispatch_barrier_sync(sself.barrierQueue, ^{
                                                        callbacksForURL = [sself.URLCallbacks[url] copy];
                                                        if (finished) {
                                                            [sself.URLCallbacks removeObjectForKey:url];
                                                        }
                                                    });
                                                    for (NSDictionary *callbacks in callbacksForURL) {
                                                        SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                        if (callback) callback(image, data, error, finished);
                                                    }
                                                }
                                                cancelled:^{
                                                    SDWebImageDownloader *sself = wself;
                                                    if (!sself) return;
                                                    dispatch_barrier_async(sself.barrierQueue, ^{
                                                        [sself.URLCallbacks removeObjectForKey:url];
                                                    });
                                                }];
operation.shouldDecompressImages = wself.shouldDecompressImages;

if (wself.urlCredential) {
    operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
    operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}

if (options & SDWebImageDownloaderHighPriority) {
    operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
    operation.queuePriority = NSOperationQueuePriorityLow;
}

[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
    // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
    [wself.lastAddedOperation addDependency:operation];
    wself.lastAddedOperation = operation;
}

先創(chuàng)建了 NSMutableURLRequest 請求對象,然后調(diào)用 SDWebImageDownloaderOperation (繼承 NSOperation)的方法:

- (id)initWithRequest:(NSURLRequest *)request
            inSession:(NSURLSession *)session
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock;

在 progressBlock 的回調(diào)方法里,會通過 sself.URLCallbacks 取出這個(gè) URL 所有 kProgressCallbackKey 的回調(diào)方法,并將獲取到的 receivedSizeexpectedSize 的值傳入這些方法中調(diào)用。
在 completedBlock 的回調(diào)方法里和 progressBlock 中的一樣,取出 kCompletedCallbackKey 對應(yīng)的回調(diào)方法,將獲取到的 image ,data,error,finished 的值傳入方法中調(diào)用,還會刪除 sself.URLCallbacks 中這個(gè) URL 的回調(diào)數(shù)組 ,保障這個(gè) URL 下次可以重新創(chuàng)建新請求。
在 cancelBlock 中則只是刪除掉 sself.URLCallbacks 中這個(gè) URL 的回調(diào)數(shù)組。
在往下是,給 operation 設(shè)置是否應(yīng)該解壓圖片的屬性,解壓圖片會提高下載和緩存的性能,但是會消耗較多的內(nèi)存,如果程序因?yàn)檎加脙?nèi)存過多而閃退則要把這個(gè)屬性設(shè)置成 NO。
設(shè)置 operation 請求的 NSURLCredential ,用于在請求過程中,服務(wù)端要求驗(yàn)證客戶端的憑證 - (void)URLSession: task: didReceiveChallenge: completionHandler: 。
再往后,是設(shè)置 NSOperation 的操作優(yōu)先級。[wself.downloadQueue addOperation:operation]; 是將操作任務(wù)加到 NSOperationQueue 隊(duì)列中,開始任務(wù)。最后是設(shè)置操作的執(zhí)行順序,默認(rèn)是 FIFO 的先進(jìn)先出的模式,也可以改成 LIFO 后進(jìn)先出的棧模式,實(shí)現(xiàn)的方法就是添加依賴,前面的操作依賴后面的操作。設(shè)置完之后,則 return 這個(gè) operation。
到此,SDWebImageDownloader 的這個(gè)核心方法就介紹完了。
還有一點(diǎn),下載的請求是使用的 NSURLSessionSDWebImageDownloaderNSURLSession 的 delegate 設(shè)置成自己,統(tǒng)一接收這些回調(diào)方法。在這些回調(diào)方法中,會返回一個(gè) NSURLSessionDataTask 通過這個(gè) dataTask 的 taskIdentifier ,我們就可以在 self.downloadQueue.operations 中找到回調(diào)方法對應(yīng)的 operation (SDWebImageDownloaderOperation),每個(gè) operation 中都有這些代理方法,這樣在 SDWebImageDownloader 統(tǒng)一接收的回調(diào)中用找到的 operation 調(diào)用當(dāng)前的這個(gè)代理方法,把參數(shù)傳到對應(yīng)的 operation 中。
代碼如下:

- (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
    SDWebImageDownloaderOperation *returnOperation = nil;
    for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
        if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
            returnOperation = operation;
            break;
        }
    }
    return returnOperation;
}
#pragma mark NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];

    [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
...

SDWebImageDownloaderOperation- (id)initWithRequest:(NSURLRequest *)request inSession: options: progress: completed: cancelled:; 是下載圖片的關(guān)鍵代碼,下面就來看下 SDWebImageDownloaderOperation 這個(gè)類。

操作單元 SDWebImageDownloaderOperation

SDWebImageDownloaderOperation 繼承自 NSOperation ,并且實(shí)現(xiàn)了 <SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate> 這三個(gè)協(xié)議。繼承 NSOperation 的子類執(zhí)行任務(wù)的代碼都寫在 - (void)start; 或者 - (void)main; 中,我們就從 SDWebImageDownloaderOperation 重寫的 - (void)start; 方法入手。

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif

這段代碼的意思是,如果程序進(jìn)入后臺會給程序一段時(shí)間,完成未完成的任務(wù),如果時(shí)間到了任務(wù)還是沒有完成則取消這個(gè)任務(wù)。

NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    sessionConfig.timeoutIntervalForRequest = 15;
    
    /**
     *  Create the session for this task
     *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
     *  method calls and completion handler calls.
     */
    self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                      delegate:self
                                                 delegateQueue:nil];
    session = self.ownedSession;
}

self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
self.thread = [NSThread currentThread];

self.unownedSession 是從 SDWebImageDownloader 中傳進(jìn)來的,而如果沒有傳進(jìn)來 self.unownedSession 則自己創(chuàng)建一個(gè) self.ownedSession,這個(gè) self.ownedSession 設(shè)置的代理是自己,回調(diào)的代理方法直接調(diào)用這個(gè)類里的, 而self.unownedSession 傳進(jìn)來的這種,代理方法就是通過上面介紹過的方式調(diào)用到這個(gè)類的。上面注釋的意思是為 task 創(chuàng)建一個(gè) session,delegateQueue 中傳入一個(gè) nil,這樣 session 就會創(chuàng)建一個(gè)串行的操作隊(duì)列來執(zhí)行所有的代理方法和完成處理的調(diào)用。
繼續(xù)看代碼:

[self.dataTask resume];

if (self.dataTask) {
    if (self.progressBlock) {
        self.progressBlock(0, NSURLResponseUnknownLength);
    }
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
    });
}
else {
    if (self.completedBlock) {
        self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
    }
}

resume 開啟這個(gè)任務(wù),調(diào)用下 self.progressBlock 傳入初始的值,然后在主線程發(fā)送一個(gè)開始下載的通知,如果沒有 self.dataTask 則調(diào)用 self.completedBlock 返回一個(gè) NSError 。

下面我們在簡單說下 SDWebImageDownloaderOperation 類中這兩個(gè) NSURLSessionTaskDelegateNSURLSessionDataDelegate 協(xié)議的代理方法。

#pragma mark NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler

在這個(gè)方法里檢查下 response 的狀態(tài)碼,不正確的話取消任務(wù),completedBlock 回調(diào) NSError 。正確的話,獲取下載數(shù)據(jù)的總大小 expectedContentLength ,并調(diào)用 self.progressBlock 。還會創(chuàng)建保存數(shù)據(jù)流的 NSMutableData 對象,self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; 。

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data

將每次接收到的數(shù)據(jù) data 拼接到之前創(chuàng)建好的 self.imageData 中去,[self.imageData appendData:data]; 。如果 option 的要求是 SDWebImageDownloaderProgressiveDownload 則在這里把已有的數(shù)據(jù) self.imageData 轉(zhuǎn)成 image ,通過 self.completedBlock 回調(diào)出去,注意 finished 參數(shù)是 NO。

if (self.progressBlock) {
    self.progressBlock(self.imageData.length, self.expectedSize);
}

調(diào)用 self.progressBlock 將下載進(jìn)度回調(diào)出去。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;

請求完成的回調(diào)方法,有 error 則 self.completedBlock(nil, nil, error, YES); ,沒有則將 self.imageData 轉(zhuǎn)成 image 回調(diào)出去 completionBlock(image, self.imageData, nil, YES); ,當(dāng)然這里面涉及很多處理的細(xì)節(jié)和其他情況的判斷,就先不說了。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;

這段則是針對不同的鑒定場景返回不一樣的策略。

至此,SDWebImageDownloaderOperation 中的這些代理方法就簡單的介紹完了。

圖片緩存 SDImageCache

SDImageCache 包括內(nèi)存緩存和磁盤緩存,內(nèi)存緩存使用的是繼承自 NSCacheAutoPurgeCache ,而磁盤緩存就是基于文件的讀寫。
先查看 SDImageCache 的接口,看下都包括哪些功能,然后一一講解代碼。
存儲的功能:

- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key;

這四個(gè)方法的前兩個(gè)直接調(diào)用的第三個(gè),所以我們從第三個(gè)方法入手。
看代碼:

// if memory cache is enabled
if (self.shouldCacheImagesInMemory) {
    NSUInteger cost = SDCacheCostForImage(image);
    [self.memCache setObject:image forKey:key cost:cost];
}

如果內(nèi)存緩存可用,就將圖片通過 NSCache 的接口 - (void)setObject: forKey: cost: ; 存入。計(jì)算 cost 的方法是:

FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
    return image.size.height * image.size.width * image.scale * image.scale;
}

也就是一張圖片的像素?cái)?shù)量。
如果需要存入磁盤,一般情況下我們是將 imageData 直接存入的,但是如果 recalculate 的值是 YES ,或者沒有 imageData,那我們就需要將 image 轉(zhuǎn)成 NSData 存入磁盤。具體的實(shí)現(xiàn)是判斷這個(gè) image 有沒有透明通道或者它的前八個(gè)字節(jié)是不是規(guī)定的 PNG 那固定的八個(gè)字節(jié),如果是則就調(diào)用 UIImagePNGRepresentation 方法轉(zhuǎn)成 NSData ,如果不是那就調(diào)用 UIImageJPEGRepresentation 這個(gè)方法。有了 data 之后,就要調(diào)用那四個(gè)存儲方法的第四個(gè) storeImageDataToDisk 。
通過 key 和 _diskCachePath 得到緩存文件的具體路徑,在使用 NSFileManager- (BOOL)createFileAtPath: contents: attributes: ; 方法,將數(shù)據(jù)寫入磁盤中。

// disable iCloud backup
    if (self.shouldDisableiCloud) {
        [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
    }

這段代碼是避免該文件被 iCloud 備份。
這些讀寫操作都放到了 SDImageCache 的一個(gè)串行隊(duì)列中,ioQueue 。我覺得是因?yàn)?_fileManager 是自己創(chuàng)建的:

dispatch_sync(_ioQueue, ^{
    _fileManager = [NSFileManager new];
});

是為了保障它的線程安全,在 SDImageCache 這個(gè)類的所有文件讀寫操作,都會放到 ioQueue 這個(gè)隊(duì)列執(zhí)行。而 [NSFileManager defaultManager] 是系統(tǒng)提供,本身就是線程安全的。

查詢的功能:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;

第一個(gè)查詢方法,在講 SDWebImageManager 時(shí)已經(jīng)講過了。
第二個(gè)方法,就是調(diào)用的 NSCache 中的 - (nullable ObjectType)objectForKey: 的方法。
第三個(gè)方法中,會先到內(nèi)存緩存去查找,如果沒有命中,則去磁盤緩存中查找,大概就是通過 key 獲取具體的路徑找到對應(yīng)的文件取出 NSData ,在經(jīng)過一些處理轉(zhuǎn)成 image 返回。

刪除的功能:

- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

前三個(gè)方法都是調(diào)用的第四個(gè),所以我們看第四個(gè)方法就好了。
如果有內(nèi)存緩存則調(diào)用 NSCache 中的 - (void)removeObjectForKey: ,如果 fromDisk 為 YES,則調(diào)用 NSFileManager- (BOOL)removeItemAtPath: error: 方法,刪除指定緩存文件的路徑即可。

清除的功能:

- (void)clearMemory;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;

第一個(gè)方法直接調(diào)用 NSCache- (void)removeAllObjects; 。第二個(gè)方法,直接調(diào)用了 NSFileManager- (BOOL)removeItemAtPath: error: 刪除指定緩存目錄的路徑即可。第三個(gè)方法調(diào)用的第二個(gè)方法。

清理的功能:

- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;

清理緩存就是清理掉一些過期的文件和超最大緩存大小限制的文件。
看第一個(gè)方法,首先獲取磁盤緩存的路徑 URL。然后通過以下代碼獲取所有緩存文件的一些屬性:

NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                           includingPropertiesForKeys:resourceKeys
                                                              options:NSDirectoryEnumerationSkipsHiddenFiles
                                                         errorHandler:NULL];

這些屬性分別是,是否是目錄,文件的修改日期和文件大小。
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge]; 這一句則是獲取緩存過期的日期。
然后 for-in 遍歷 fileEnumerator

NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
    NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

    // Skip directories.
    if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
        continue;
    }

    // Remove files that are older than the expiration date;
    NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
    if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
        [urlsToDelete addObject:fileURL];
        continue;
    }

    // Store a reference to this file and account for its total size.
    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
    currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
    [cacheFiles setObject:resourceValues forKey:fileURL];
}

獲取文件路徑的屬性字典,如果是目錄則跳過,比較修改日期和過期日期哪個(gè)更晚一些,如果是過期日期則說明該文件過期,放入 urlsToDelete 數(shù)組中。將文件大小累加到 currentCacheSize 上,并將不是過期的這些緩存文件記錄到 cacheFiles 中,key 是文件的 URL ,value 是對應(yīng)的屬性字典。
之后,遍歷 urlsToDelete 數(shù)組刪除這些過期文件:

for (NSURL *fileURL in urlsToDelete) {
    [_fileManager removeItemAtURL:fileURL error:nil];
}

然后,判斷沒有過期的這些文件的總大小有沒有超過最大的緩存大小 self.maxCacheSize
如果有的話,將 cacheFiles 里的 value 按照文件的修改日期進(jìn)行排序,返回一個(gè)排好序的數(shù)組。取 self.maxCacheSize 大小的一半,作為清理緩存的界限 const NSUInteger desiredCacheSize = self.maxCacheSize / 2;。遍歷排序后的數(shù)組,一個(gè)個(gè)文件刪除,刪除一個(gè)就從之前的總緩存文件大小的值減去刪除后的文件大小,再比較有沒有小于清理緩存的界限值 desiredCacheSize。如果小于了,則跳出循環(huán)。最后在主線程回調(diào) completionBlock(); 。這樣就達(dá)到了清理磁盤緩存的目的。

計(jì)算緩存大?。?/p>

- (NSUInteger)getSize;
- (NSUInteger)getDiskCount;
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;

第一個(gè)方法就是遍歷緩存目錄的所有文件,獲取這些文件路徑,通過 [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] 獲得一個(gè)字典在通過 fileSize 方法獲取文件大小,累加起來就是緩存的大小。
第二個(gè)和第三個(gè)方法都是獲取指定緩存路徑的 NSDirectoryEnumerator 遍歷取對應(yīng)的值,和上面相差不大,不在贅述。

查詢緩存是否存在:

- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;

這些方法的實(shí)現(xiàn)基本就是調(diào)用 exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]]; 這個(gè)方法,不在贅述。

最后說下 clearMemorycleanDisk ,backgroundCleanDisk 的調(diào)用時(shí)機(jī),在 - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory{} 這個(gè)初始化方法中,注冊了三個(gè)通知分別是:

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(clearMemory)
                                             name:UIApplicationDidReceiveMemoryWarningNotification
                                           object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(cleanDisk)
                                             name:UIApplicationWillTerminateNotification
                                           object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundCleanDisk)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];

報(bào)內(nèi)存警告時(shí)調(diào)用 clearMemory 清除內(nèi)存緩存,程序即將終止的時(shí)候調(diào)用 cleanDisk 清理過期或超大小限制的磁盤緩存,而程序進(jìn)入后臺的時(shí)候,調(diào)用 backgroundCleanDisk ,在后臺執(zhí)行 cleanDiskWithCompletionBlock 清理任務(wù)。

至此,SDImageCache 的大部分方法就講解完了。

結(jié)束

SDWebImage 這個(gè)庫的基本思路就說完了,其實(shí)里面還有諸多的細(xì)節(jié)需要學(xué)習(xí),比如圖片處理,性能優(yōu)化,內(nèi)存管理等。閱讀優(yōu)秀的開源代碼,會有一種探索的樂趣,不論從大的整體結(jié)構(gòu)還是小的實(shí)現(xiàn)細(xì)節(jié)上,都能學(xué)到很多東西。

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

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

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