SDWebImage4.0源碼閱讀筆記(二)

緊接著上一篇文章,在這篇文章里面,我會(huì)先從 SDWebImageManager 中的 loadImageWithURL 這個(gè)方法入手來(lái)理解在 SDWebImageManager 這一步里面到底做了什么事情。讓咱們對(duì)整個(gè)圖片加載流程有個(gè)更加詳細(xì)的理解,隨后再分析這個(gè)類中其它的一些方法。當(dāng)然也會(huì)順帶把 SDImageCache 的作用詳細(xì)分析一遍。

SDWebImageManager

SDWebImageManager 同時(shí)管理 SDImageCache 和 SDWebImageDownloader兩個(gè)類,它是這一層的中樞。在加載圖片任務(wù)開(kāi)始的時(shí)候,SDWebImageManager 首先訪問(wèn) SDImageCache 來(lái)查詢是否存在緩存,如果有緩存,直接返回緩存的圖片。如果沒(méi)有緩存,就命令SDWebImageDownloader來(lái)下載圖片,下載成功后,存入緩存,顯示圖片。以上是SDWebImageManager大致的工作流程。

先介紹一下這個(gè)類中比較重要的屬性:

//負(fù)責(zé)緩存相關(guān)的類
@property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache;
//負(fù)責(zé)下載相關(guān)的類
@property (strong, nonatomic, readwrite, nonnull) SDWebImageDownloader *imageDownloader;
//記錄加載失敗的圖片URL
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
//記錄當(dāng)前正在進(jìn)行的任務(wù),可以看到這個(gè)任務(wù)是 SDWebImageCombinedOperation,下面介紹
@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageCombinedOperation *> *runningOperations;

1.SDWebImageCombinedOperation
可以看到這個(gè)類其實(shí)就是實(shí)現(xiàn)了上篇文章咱們提到的 SDWebImageOperation 協(xié)議,而這個(gè)協(xié)議也僅僅只有一個(gè)cancle方法,在最開(kāi)始看這里的時(shí)候我心里也有疑問(wèn),這里 SDWebImageCombinedOperation 也有個(gè)cancleBlock,協(xié)議也有個(gè)cancle方法,這兩者到底有什么關(guān)系呢???

這里先介紹結(jié)論,后面上代碼:協(xié)議里面的 cancle 方法做的事情是將查詢緩存任務(wù)和下載任務(wù)一并取消掉,而這里面的 cancelBlock 所做的事情其實(shí)是去取消下載任務(wù),換句話說(shuō),其實(shí)就是協(xié)議里面的 cancle 方法會(huì)調(diào)用這個(gè)cancleBlock和額外的取消查詢緩存。而額外的取消查詢緩存的操作也是因?yàn)檫@個(gè) CombinedOperation中包含了查詢緩存的cacheOperation,所以起名叫CombinedOperation,包含了下載任務(wù)和緩存任務(wù)。

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;//取消查緩存和下載操作
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
@end

2.SDWebImageManagerDelegate

@protocol SDWebImageManagerDelegate <NSObject>

@optional
//決定當(dāng)緩存中沒(méi)有找到圖片時(shí)是否需要下載,返回 NO 的話代表即使在緩存中沒(méi)有找到圖片也不去下載
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;

//允許圖片在剛下載回來(lái)并且在還沒(méi)放入緩存之前對(duì)圖片進(jìn)行 transform ,返回變換后的圖片
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;

@end

接下來(lái)進(jìn)入這個(gè)非常重要的 loadImageWithURL

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
    ...
    //保護(hù)措施,如果一不小心傳了一個(gè)string類型過(guò)來(lái),直接將它轉(zhuǎn)為NSURL
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    ...
    //創(chuàng)建一個(gè)任務(wù),并同時(shí)創(chuàng)建一個(gè)weak引用
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    //這個(gè)failedURLs其實(shí)就是黑名單,判斷當(dāng)前傳進(jìn)來(lái)的URL是不是已經(jīng)在黑名單里面了,后面有用
    BOOL isFailedUrl = NO;
    if (url) {
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }
    // url的長(zhǎng)度為0 || (下載失敗后進(jìn)入黑名單 && 確實(shí)在黑名單中)
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
    // 直接回調(diào)完成block
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }
    //沒(méi)有上述問(wèn)題,那就直接把創(chuàng)建的任務(wù)加載到當(dāng)前任務(wù)表中吧
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    //根據(jù)URL獲取緩存的key
    NSString *key = [self cacheKeyForURL:url];
    
    //調(diào)用 imageCache 去查詢緩存,并返回該任務(wù)
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {       
        if (operation.isCancelled) {
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
         //(沒(méi)有緩存圖片) || (即使有緩存圖片,也需要更新緩存圖片) || (代理沒(méi)有響應(yīng)imageManager:shouldDownloadImageForURL:消息,默認(rèn)返回yes,需要下載圖片)|| (imageManager:shouldDownloadImageForURL:返回yes,需要下載圖片)
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            //1. 存在緩存圖片 && 即使有緩存圖片也要下載更新圖片
            if (cachedImage && options & SDWebImageRefreshCached) {             
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }
            // 2. 如果不存在緩存圖片,或者即使有緩存圖片也要下載更新圖片的情況,直接去下載
          
            .....
          
           //開(kāi)啟下載器下載,subOperationToken 用來(lái)標(biāo)記當(dāng)前的下載任務(wù),便于被取消            
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // 1. 如果任務(wù)被取消,則什么都不做,避免和其他的completedBlock重復(fù)
    
                } else if (error) {
                    //2. 如果有錯(cuò)誤
                    //2.1 在completedBlock里傳入error
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
                    //2.2 在錯(cuò)誤url名單中添加當(dāng)前的url
                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost
                        && error.code != NSURLErrorNetworkConnectionLost) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                     //3.1 下載成功,如果需要下載失敗后重新下載,則將當(dāng)前url從失敗url名單里移除
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    //是否可以緩存到磁盤(pán)
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    //(即使緩存存在,也要刷新圖片) && 緩存圖片 && 不存在下載后的圖片:不做操作
                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    }
                    //(下載圖片成功 && (不是動(dòng)圖||處理動(dòng)圖) && (下載之后,在緩存之前先處理圖片)
                    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];
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                //緩存圖片
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            //回調(diào)
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        //(圖片下載成功并結(jié)束) 動(dòng)圖
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }
                //把當(dāng)前Operation從 表示正在執(zhí)行的Operation中移除
                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            //指定SDWebImageCombinedOperation的取消事件,這里可以看出cancleBlock僅僅是用來(lái)cancle下載任務(wù)的
            operation.cancelBlock = ^{
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self safelyRemoveOperationFromRunning:strongOperation];
            };
        }
        else if (cachedImage) {//有緩存圖片
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        } else {
            // 沒(méi)有緩存圖片,下載也被代理給終止了
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;
}

至此,這個(gè)最復(fù)雜的函數(shù),也算是一層一層理清楚了,主要是判斷有點(diǎn)多,耐心一點(diǎn)還是沒(méi)有問(wèn)題的,接下來(lái)我們?cè)侔堰@個(gè)類里面的一些別的函數(shù)簡(jiǎn)單看一下 ,然后再進(jìn)入到SDWebImageCache這個(gè)很重要的類中去。

這里面的方法大多數(shù)都是對(duì) SDWebImageCache中的方法進(jìn)行了一層封裝,間接的調(diào)用 SDWebImageCache 中的方法實(shí)現(xiàn)功能

//將圖片和他對(duì)應(yīng)的URL存入緩存,url是拿來(lái)當(dāng)key的
- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url;

//取消正在加載圖片的所有任務(wù)
- (void)cancelAll;

//判斷當(dāng)前UIImageView或者button是否有正在加載圖片的任務(wù)
- (BOOL)isRunning;

//根據(jù)URL去異步檢查它對(duì)應(yīng)的圖片是否被緩存了,不論是磁盤(pán)還是內(nèi)存都算,回調(diào)總是在主隊(duì)列
- (void)cachedImageExistsForURL:(nullable NSURL *)url
                     completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

//根據(jù)URL區(qū)異步僅僅檢查圖片是否被緩存在了磁盤(pán)上
- (void)diskImageExistsForURL:(nullable NSURL *)url
                   completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

//根據(jù)URL返回緩存的key
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url;

SDImageCache

從 SDWebImageManager 中調(diào)用的很重要的這查詢緩存入手:
先介紹一下SDImageCacheTypeNone所對(duì)應(yīng)的 SDImageCacheType,可以理解為緩存所在位置:

typedef NS_ENUM(NSInteger, SDImageCacheType) {
   //沒(méi)有緩存
    SDImageCacheTypeNone,
   //緩存在磁盤(pán)
    SDImageCacheTypeDisk,
   //緩存在內(nèi)存
    SDImageCacheTypeMemory
};
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    //先判斷有沒(méi)有key,如果沒(méi)有的話,直接走doneBlock
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // 首先先查詢內(nèi)存緩存,內(nèi)存緩存其實(shí)是NSCache,雖然這里封裝了一層但其實(shí)就是調(diào)用objectForKey那個(gè)方法
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    
    if (image) {
        NSData *diskData = nil;
        //如果是gif,就拿到data,后面要傳到doneBlock里。不是gif就傳nil
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }         
        //因?yàn)閳D片有緩存可供直接使用,所以不用實(shí)例化NSOperation來(lái)進(jìn)行耗時(shí)的磁盤(pán)查詢,直接范圍nil
        return nil;
    }
  //如果內(nèi)存中沒(méi)找到緩存,那肯定只有查磁盤(pán)了,接下來(lái)進(jìn)行磁盤(pán)緩存查詢,實(shí)例化NSOperation
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
//self.ioQueue作者自己創(chuàng)建了一個(gè)查詢隊(duì)列,在進(jìn)行異步查詢的時(shí)候,因?yàn)楹瘮?shù)下面的同步return operation 
//操作可能先返回出去,于是就有可能先返回出去的operation被cancle掉,因此作者在這里加了一層判斷,看是否被取消掉了,這層考慮非常嚴(yán)謹(jǐn)
        if (operation.isCancelled) {
            // 如果被cancle掉了,那么什么都不用做了
            return;
        }
 //放到autoreleasepool中去查磁盤(pán)緩存,降低內(nèi)存峰值
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            //在磁盤(pán)中找到了圖片 && 要緩存到內(nèi)存中
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                //緩存到內(nèi)存中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            //完成回調(diào)
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

磁盤(pán)清理緩存邏輯概述:先清除過(guò)期的文件;然后判斷此時(shí)的緩存文件大小是否小于設(shè)置的最大大小。若大于最大大小,則進(jìn)行第二輪的清掃,清掃到緩存文件大小為設(shè)置的最大大小的一半。

dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; // 緩存文件的路徑
        // 將要獲取文件的3個(gè)屬性(URL是否為目錄;內(nèi)容最后更新日期;文件總的分配大小)
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey,NSURLTotalFileAllocatedSizeKey];
        // 使用目錄枚舉器獲取緩存文件有用的屬性
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;
        // 枚舉緩存目錄的所有文件,此循環(huán)有兩個(gè)目的:
        // 1.清除超過(guò)過(guò)期日期的文件;
        // 2.為下面根據(jù)大小清理磁盤(pán)做準(zhǔn)備
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL]; // 傳入想獲得的該URL路徑文件的屬性數(shù)組,得到這些屬性字典。

            // 若該URL是目錄,則跳過(guò)。
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }
            // 清除過(guò)期文件
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL]; // 把過(guò)期的文件url暫時(shí)先置于urlsToDelete數(shù)組中
                continue;
            }
        // 計(jì)算文件總的大小并保存保留下來(lái)的文件的引用。
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }
        // 如果剩下的磁盤(pán)緩存文件仍然大于我們?cè)O(shè)置的最大大小,則要執(zhí)行以大小為基礎(chǔ)的第二輪清除
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // 此輪清理的目標(biāo)是最大緩存的一半
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
            // 以剩下的文件最后更新時(shí)間排序(最老的最先被清除)
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }
                                                                        // 刪除已排好序的文件,直到達(dá)到最大緩存限制的一半
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });

介紹完了 SDImageCache 在整個(gè)流程中的主要任務(wù)之后,咱們?cè)賮?lái)詳細(xì)看看SDImageCache這個(gè)類的其他的一些東西:

屬性

//內(nèi)存緩存對(duì)應(yīng)的NSCache
@property (strong, nonatomic, nonnull) NSCache *memCache;
//磁盤(pán)緩存路徑
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
//自定義的隊(duì)列,查詢磁盤(pán)緩存的操作就放在這個(gè)隊(duì)列里
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;
//可以理解為cache的配置信息,下面再講
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
//支持緩存的最大空間,不過(guò)據(jù)說(shuō)設(shè)置這個(gè)也不是絕對(duì)的,詳情看NSCache就知道了
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//支持緩存的最大對(duì)象數(shù),默認(rèn)不限制
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
/* ================ SDImageCacheConfig =====================*/
//是否解壓下載的圖片,拿空間換時(shí)間,默認(rèn)yes
@property (assign, nonatomic) BOOL shouldDecompressImages;
//靜止iCloud備份,默認(rèn)yes
@property (assign, nonatomic) BOOL shouldDisableiCloud;
//允許使用內(nèi)存來(lái)緩存圖片,默認(rèn)yes
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
//緩存的最長(zhǎng)時(shí)間 默認(rèn)一周
@property (assign, nonatomic) NSInteger maxCacheAge;
//緩存的最大值,單位byte
@property (assign, nonatomic) NSUInteger maxCacheSize;

方法:

//磁盤(pán)緩存路徑,默認(rèn)是在沙盒Cache下面的 com.hackemist.SDWebImageCache文件夾
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace;
//異步緩存圖片到內(nèi)存與磁盤(pán)
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;        
//異步檢查磁盤(pán)中是否有緩存        
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;
//查詢緩存,就是Manager中使用的那個(gè)
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;
....
....
....
其余的很多方法都在源碼中都有很詳細(xì)的注釋且理解比較容易,這里就不做過(guò)多的介紹,讀者可以自行閱讀

問(wèn)題:

1.在緩存圖片的時(shí)候如何計(jì)算圖片的占用空間的?

//SDImageCache.m
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_MAC
    return image.size.height * image.size.width;
#elif SD_UIKIT || SD_WATCH
    return image.size.height * image.size.width * image.scale * image.scale;
#endif
}

很明顯在iOS上是用圖片的 長(zhǎng) * 寬 * 圖片比例 * 圖片比例

2.什么時(shí)候自動(dòng)清理緩存呢?(手動(dòng)調(diào)用清理接口的另當(dāng)別論)

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

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

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


很明顯可以看到,在初始化 SDImageCache 的時(shí)候注冊(cè)了這么幾個(gè)通知,所以結(jié)論如下:

  • 當(dāng)收到內(nèi)存警告時(shí),會(huì)清理所有內(nèi)存緩存

  • 程序終止或者進(jìn)入后臺(tái)時(shí),刪除磁盤(pán)上過(guò)期的緩存

2.內(nèi)存的key是什么,存在磁盤(pán)上的文件名是什么?

內(nèi)存緩存的key直接就是圖片的URL,而存磁盤(pán)上的緩存文件名是根據(jù)URL生成的MD5

3.在存磁盤(pán)的時(shí)候如何區(qū)分圖片的文件格式?

==============================< NSData+ImageContentType.h >==================
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        case 0x52:
            // R as RIFF for WEBP
            if (data.length < 12) {
                return SDImageFormatUndefined;
            }
            
            NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return SDImageFormatWebP;
            }
    }
    return SDImageFormatUndefined;
}

很明顯可以看到是取服務(wù)端返回的數(shù)據(jù)的第一個(gè)字節(jié)來(lái)做判斷

4.哪些事情是放在這個(gè)自定義的 ioQueue 上做的?

  • 清理磁盤(pán)緩存
  • 計(jì)算磁盤(pán)緩存的大小的時(shí)候
  • 查詢磁盤(pán)緩存的時(shí)候
  • 初始化 NSFileManager 的時(shí)候

5.初始化NSFileManager的時(shí)候?yàn)槭裁匆旁谧约旱拇嘘?duì)列,然后同步執(zhí)行呢?為什么要自己去 new 一個(gè)Manager實(shí)例而不是使用 NSFileManager 的defaultManager呢?

解釋一下:同步放在串行隊(duì)列中的任務(wù)其實(shí)目的就是為了線程同步,相當(dāng)于為block中的代碼進(jìn)行加同步鎖的操作;在defaultManager文檔中也有提到,如果你想使用 NSFileManagerDelegate 來(lái)回調(diào)一些相關(guān)操作通知的話,組好是自己創(chuàng)建和初始化一個(gè) NSFileManager 的實(shí)例,可是SD貌似很明顯沒(méi)用到相關(guān)delegate;通過(guò)多方查找:終于找到一篇文章(此處多謝鵬大神和我一起探究),其實(shí)問(wèn)題就是 NSFileManager 的 defaultManager并不是線程安全的,所以作者才會(huì)自己實(shí)例化對(duì)象,并把它放在同步線程中;

NSFileManager

至于為什么要通過(guò)這種方式來(lái)進(jìn)行線程同步,大家其實(shí)可以參考 《Effective Objective - C 2.0》中的第41條,說(shuō)白了其實(shí)就是 @synchronized()的效率沒(méi)有這個(gè)高啦。

總結(jié),到這里就把 SDWebImageManager 和 SDImageCache 都大致理了一遍,在下篇文章咱們主要去分析下載模塊即SDWebImageDownloader涉及到的一些東西

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容