SDWebImage 源碼閱讀(緩存)

在 SDWebImage 中,設(shè)計(jì)了兩種緩存
1.SDMemoryCache:它繼承自 NSCache 用來(lái)實(shí)現(xiàn)內(nèi)存緩存
2.NSFileManager:使用文件的方式來(lái)實(shí)現(xiàn)磁盤(pán)緩存

  • 先來(lái)看一下 SDImageCache 的內(nèi)存緩存的實(shí)現(xiàn)
@interface SDMemoryCache <KeyType, ObjectType> ()
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; 
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; 
@end
  1. weakCache:它是一個(gè)NSMapTable的對(duì)象,那么它和字典都有哪些區(qū)別呢?a> 它是可變的;b> 可以在添加value的時(shí)候?qū)alue進(jìn)行復(fù)制;c> 可以通過(guò)弱引用來(lái)持有keys和values,所以當(dāng)key或者value被deallocated的時(shí)候,所存儲(chǔ)的實(shí)體也會(huì)被移除;
  2. weakCacheLock:它是一個(gè)鎖,用來(lái)保證對(duì)weakCache操作時(shí)的線程安全,所以在對(duì)SDWebImage的緩存研究時(shí),我們可以忽略它
  3. SDMemoryCache:它自己是NSCache,也會(huì)對(duì)圖片進(jìn)行內(nèi)存緩存,并且它還是線程安全的

問(wèn)題:既然NSCache已經(jīng)可以實(shí)現(xiàn)圖片的內(nèi)存緩存了,為啥還要加一個(gè)NSMapTable來(lái)再緩存一次呢?
我想這可能是因?yàn)镹SCache在收到內(nèi)存警告時(shí)會(huì)自動(dòng)釋放緩存,當(dāng)然這是沒(méi)有問(wèn)題的,但坑的是它的釋放是沒(méi)有順序的,所以可能是剛存入的數(shù)據(jù)對(duì)象被清理了,而不是我們希望的“先進(jìn)先出”順序,在實(shí)際情況中,往往是最新存入的數(shù)據(jù)被再次用到的可能性比較大,所以作者在NSCache的基礎(chǔ)上又加了一個(gè)NSMapTable緩存,這應(yīng)該是為了提高內(nèi)存緩存的命中率吧

NSCache的相關(guān)內(nèi)容可以參考這篇文章 http://nshipster.cn/nscache/

我們?cè)賮?lái)看一下具體的代碼實(shí)現(xiàn),作者重寫(xiě)了NSCache的方法來(lái)實(shí)現(xiàn)了NSMapTable的緩存,為了方便閱讀,我刪除了線程安全的代碼

- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
    // 先將對(duì)象緩存的 NSCache 中
    [super setObject:obj forKey:key cost:g];
    if (key && obj) {
        // 如果存在key和value,則再存到NSMapTable中
        [self.weakCache setObject:obj forKey:key];
    }
}

// 從該方法中,我們可以看到兩次的獲取緩存,說(shuō)明NSMapTable確實(shí)是用來(lái)提高緩存命中率的
- (id)objectForKey:(id)key {
    // 在NSCache中獲取緩存對(duì)象
    id obj = [super objectForKey:key];
    if (key && !obj) {
        // 如果沒(méi)有獲取的緩存,則再次在NSMapTable中獲取緩存
        obj = [self.weakCache objectForKey:key];
        if (obj) {
            // 如果從NSMapTable中獲取到了緩存,則再次存入NSCache中
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[UIImage class]]) {
                cost = SDCacheCostForImage(obj);
            }
            [super setObject:obj forKey:key cost:cost];
        }
    }
    return obj;
}
  • SDImageCache 的磁盤(pán)緩存的實(shí)現(xiàn)
    磁盤(pán)緩存是用NSFileManager來(lái)實(shí)現(xiàn)的,我們直接看緩存函數(shù)
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    // 判斷將要緩存的路徑是否存在,如果不存在,則創(chuàng)建一個(gè)
    if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
        [self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    // 獲取默認(rèn)的緩存路徑
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // 將路徑轉(zhuǎn)為url
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    // 將圖片數(shù)據(jù)寫(xiě)入文件,并保存
    [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
    
    // 禁用icloud備份,默認(rèn)是YES
    if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

有該函數(shù)可以看出,磁盤(pán)緩存是將圖片保存的沙盒的cache目錄下的,那么最關(guān)鍵的圖片的保存路徑是怎么來(lái)的呢?我們可以看下面這個(gè)函數(shù)

// key:這個(gè)key就是圖片的url
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    // 使用了MD5進(jìn)行加密處理
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSURL *keyURL = [NSURL URLWithString:key];
    NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
    // 所以最后的圖片保存路徑就是 "沙盒cache路徑"+"url的md5嗎"+".圖片類(lèi)型"
    return filename;
}

其它關(guān)于緩存的函數(shù)
移除過(guò)期的緩存或當(dāng)緩存到最大值時(shí)移除較早的圖片

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // 獲取緩存文件的屬性
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                       includingPropertiesForKeys:resourceKeys
                                                                          options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                     errorHandler:NULL];
        // 最早的有效緩存的時(shí)間,小于這個(gè)時(shí)間的緩存都失效了
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        // 當(dāng)前所有緩存的大小
        NSUInteger currentCacheSize = 0;
        // 存儲(chǔ)需要移除的緩存圖片的路徑
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
            // 錯(cuò)誤處理
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }
            // 通過(guò)時(shí)間來(lái)判斷出需要移除的緩存
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
            // 計(jì)算有效緩存大小
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        // 移除緩存
        for (NSURL *fileURL in urlsToDelete) {
            [self.fileManager removeItemAtURL:fileURL error:nil];
        }

        // 當(dāng)緩存大小大于設(shè)置的最多緩存控件時(shí),移除相對(duì)較早緩存
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // 只留下剩下最大緩存的一半,其它全部清除了
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // 通過(guò)緩存的時(shí)間來(lái)排序,才好移除早期的緩存
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                     }];
            // 開(kāi)始清除緩存
            for (NSURL *fileURL in sortedFiles) {
                if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
                    // 當(dāng)剩下的緩存小于最大緩存的一半時(shí),停止緩存清除
                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}
  • 在SDWebImage中我們可以對(duì)其緩存方式進(jìn)行設(shè)置,比如不需要內(nèi)存緩存、緩存最大容量等,SDWebImage 為我們提供了一個(gè)專(zhuān)門(mén)配置的對(duì)象
@interface SDImageCacheConfig : NSObject
// 是否對(duì)圖片進(jìn)行解壓縮 默認(rèn) YSE
@property (assign, nonatomic) BOOL shouldDecompressImages;
// 是否禁用icloud備份 默認(rèn) YSE
@property (assign, nonatomic) BOOL shouldDisableiCloud;
// 是否內(nèi)存緩存 默認(rèn) YSE
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
/**
 * The reading options while reading cache from disk.
 * Defaults NSDataReadingMappedIfSafe
 */
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
/**
 * The writing options while writing cache to disk.
 * Defaults NSDataWritingAtomic
 */
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
// 緩存的超時(shí)時(shí)間
@property (assign, nonatomic) NSInteger maxCacheAge;
// 最大緩存容量
@property (assign, nonatomic) NSUInteger maxCacheSize;

@end

我需要通過(guò)哪里來(lái)設(shè)置這些呢,其實(shí)SDImageCache是一個(gè)單例,所以只需我們?cè)傧螺d圖片之前取到SDImageCache單例,就可以對(duì)其參數(shù)進(jìn)行設(shè)置,如下

// 如果這幾行代碼寫(xiě)在 AppDelegate 里面,那么就可以對(duì)所有的圖片下載進(jìn)行設(shè)置
[SDImageCache sharedImageCache].config.maxCacheAge = 60 * 60 * 24 * 7; // 磁盤(pán)緩存 7天
[SDImageCache sharedImageCache].config.maxCacheSize = 0; // 磁盤(pán)緩存 這里設(shè)置為0,表示無(wú)限大
[SDImageCache sharedImageCache].config.shouldCacheImagesInMemory = true; // 開(kāi)啟內(nèi)存緩存
[SDImageCache sharedImageCache].maxMemoryCost = 0; // 內(nèi)存緩存 最大值
[SDImageCache sharedImageCache].maxMemoryCountLimit = 0; // 內(nèi)存緩存 最大數(shù)量

注意,是否需要磁盤(pán)緩存,是通過(guò)下載時(shí)傳入的,在這里并不能配置,在下載時(shí)我們需要傳入SDWebImageOptions這個(gè)參數(shù),默認(rèn)是SDWebImageRetryFailed,只有我們傳入SDWebImageCacheMemoryOnly時(shí),才不會(huì)進(jìn)行磁盤(pán)緩存,其它枚舉都會(huì)進(jìn)行磁盤(pán)緩存,例如下面UIImageView的擴(kuò)展

// 這是我們平時(shí)使用最多的函數(shù),它不需要傳入options  默認(rèn)就是 SDWebImageRetryFailed
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

// options: 如果這里傳入 SDWebImageCacheMemoryOnly,則不進(jìn)行磁盤(pán)緩存
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}

總結(jié)
SDWebImage 使用NSCache+NSMapTable來(lái)實(shí)現(xiàn)了內(nèi)存緩存,使用NSFileManager來(lái)實(shí)現(xiàn)磁盤(pán)緩存,有超時(shí)和超出容量?jī)煞N清除緩存的策略

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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