iOS-SDWebImage緩存機(jī)制

序言

本文章中講解的SDWebImage版本為4.4.4

一 存儲(chǔ)

圖片的存儲(chǔ)主要是由類SDImageCache實(shí)現(xiàn)的,主要方法如下

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    if (!image || !key) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // if memory cache is enabled
    if (self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = image.sd_memoryCost;
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                NSData *data = imageData;
                // 如果image存在,但是需要重新計(jì)算(recalculate)或者data為空
                // 那就要根據(jù)image重新生成新的data
                // 不過(guò)要是連image也為空的話,那就別存了
                if (!data && image) {
                    // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
                    SDImageFormat format;
                    if (SDCGImageRefContainsAlpha(image.CGImage)) {
                        format = SDImageFormatPNG;
                    } else {
                        format = SDImageFormatJPEG;
                    }
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                }
                [self _storeImageDataToDisk:data forKey:key];
            }
            
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}
  • 存儲(chǔ)圖片
// Make sure to call form io queue by caller
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    if (!imageData || !key) {
        return;
    }
    
    // 首先判斷disk cache的文件路徑是否存在,不存在的話就創(chuàng)建一個(gè)
    // disk cache的文件路徑是存儲(chǔ)在_diskCachePath中的
    if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
        [self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // get cache Path for image key
    // 根據(jù)image的key(一般情況下理解為image的url)組合成最終的文件路徑
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // transform to NSUrl
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    
    // 根據(jù)存儲(chǔ)的路徑(cachePathForKey)和存儲(chǔ)的數(shù)據(jù)(data)將其存放到iOS的文件系統(tǒng)
    [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
    
    // disable iCloud backup
    if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}
二 取圖片
  1. 內(nèi)存緩存使用SDImageCacheimageFromMemoryCacheForKey:取數(shù)據(jù)
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    return [self.memCache objectForKey:key];
}
  1. 從磁盤中讀取數(shù)據(jù)
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
    UIImage *diskImage = [self diskImageForKey:key];
    if (diskImage && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = diskImage.sd_memoryCost;
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }

    return diskImage;
}
  • 讀取磁盤
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
    NSData *data = [self diskImageDataForKey:key];
    return [self diskImageForKey:key data:data];
}

- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
    return [self diskImageForKey:key data:data options:0];
}

- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
    if (data) {
        UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
        image = [self scaledImageForKey:key image:image];
        if (self.config.shouldDecompressImages) {
            BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;
            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
        }
        return image;
    } else {
        return nil;
    }
}
三 刪除圖片

刪除圖片主要用到下面四個(gè)方法

  • removeImageForKeyfromDisk:withCompletion: 異步地將image從緩存(內(nèi)存緩存以及可選的磁盤緩存)中移除
  • clearMemory 清楚內(nèi)存緩存上的所有image
  • clearDisk 清除磁盤緩存上的所有image
  • cleanDisk 清除磁盤緩存上過(guò)期的image

主要由類SDImageCache實(shí)現(xiàn)

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        // 記錄遍歷的文件目錄
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];

        // Compute content date key to be used for tests
        NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
        switch (self.config.diskCacheExpireType) {
            case SDImageCacheConfigExpireTypeAccessDate:
                cacheContentDateKey = NSURLContentAccessDateKey;
                break;

            case SDImageCacheConfigExpireTypeModificationDate:
                cacheContentDateKey = NSURLContentModificationDateKey;
                break;

            default:
                break;
        }
        
        // 記錄遍歷需要預(yù)先獲取文件的哪些屬性
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];

        // This enumerator prefetches useful properties for our cache files.
        // 遞歸地遍歷diskCachePath這個(gè)文件夾中的所有目錄,此處不是直接使用diskCachePath,而是使用其生成的NSURL
        // 此處使用includingPropertiesForKeys:resourceKeys,這樣每個(gè)file的resourceKeys對(duì)應(yīng)的屬性也會(huì)在遍歷時(shí)預(yù)先獲取到
        // NSDirectoryEnumerationSkipsHiddenFiles表示不遍歷隱藏文件
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        // 獲取文件的過(guò)期時(shí)間,SDWebImage中默認(rèn)是一個(gè)星期
        // 不過(guò)這里雖然稱*expirationDate為過(guò)期時(shí)間,但是實(shí)質(zhì)上并不是這樣。
        // 其實(shí)是這樣的,比如在2015/12/12/00:00:00最后一次修改文件,對(duì)應(yīng)的過(guò)期時(shí)間應(yīng)該是
        // 2015/12/19/00:00:00,不過(guò)現(xiàn)在時(shí)間是2015/12/27/00:00:00,我先將當(dāng)前時(shí)間減去1個(gè)星期,得到
        // 2015/12/20/00:00:00,這個(gè)時(shí)間才是我們函數(shù)中的expirationDate。
        // 用這個(gè)expirationDate和最后一次修改時(shí)間modificationDate比較看誰(shuí)更晚就行。
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        // 用來(lái)存儲(chǔ)對(duì)應(yīng)文件的一些屬性,比如文件所需磁盤空間
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        // 記錄當(dāng)前已經(jīng)使用的磁盤緩存大小
        NSUInteger currentCacheSize = 0;

        // Enumerate all of the files in the cache directory.  This loop has two purposes:
        //
        //  1. Removing files that are older than the expiration date.
        //  2. Storing file attributes for the size-based cleanup pass.
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

            // Skip directories and errors. - 當(dāng)前掃描的是目錄,就跳過(guò)
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // Remove files that are older than the expiration date;  
            // 移除過(guò)期文件 - 這里判斷過(guò)期的方式:對(duì)比文件的最后一次修改日期和expirationDate誰(shuí)更晚,如果expirationDate更晚,就認(rèn)為該文件已經(jīng)過(guò)期,具體解釋見(jiàn)上面
            NSDate *modifiedDate = resourceValues[cacheContentDateKey];
            if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
            
            // Store a reference to this file and account for its total size.
            // 計(jì)算當(dāng)前已經(jīng)使用的cache大小, - 并將對(duì)應(yīng)file的屬性存到cacheFiles中
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        
        // 根據(jù)需要移除文件的url來(lái)移除對(duì)應(yīng)file
        for (NSURL *fileURL in urlsToDelete) {
            [self.fileManager removeItemAtURL:fileURL error:nil];
        }

        // If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
        // 如果我們當(dāng)前cache的大小已經(jīng)超過(guò)了允許配置的緩存大小,那就刪除已經(jīng)緩存的文件。
        // 刪除策略就是,首先刪除修改時(shí)間更早的緩存文件
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            // 直接將當(dāng)前cache大小降到允許最大的cache大小的一半
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // Sort the remaining cache files by their last modification time or last access time (oldest first).
            // 根據(jù)文件修改時(shí)間來(lái)給所有緩存文件排序,按照修改時(shí)間越早越在前的規(guī)則排序
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                                     }];

            // Delete files until we fall below our desired cache size.
            // 每次刪除file后,就計(jì)算此時(shí)的cache的大小
            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;
  
                    // 如果此時(shí)的cache大小已經(jīng)降到期望的大小了,就停止刪除文件了
                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}
四 圖片存儲(chǔ)路徑
  • 磁盤存儲(chǔ)圖片路徑
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
    return [self cachePathForKey:key inPath:self.diskCachePath];
}

// 將存儲(chǔ)的文件路徑和文件名綁定在一起,作為最終的存儲(chǔ)路徑
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}

- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    if (str == NULL) {
        str = "";
    }
    // 使用了MD5進(jìn)行加密處理
    // 開(kāi)辟一個(gè)16字節(jié)(128位:md5加密出來(lái)就是128bit)的空間
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    // 把str字符串轉(zhuǎn)換成了32位的16進(jìn)制數(shù)列(這個(gè)過(guò)程不可逆轉(zhuǎn)) 存儲(chǔ)到了r這個(gè)空間中
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSURL *keyURL = [NSURL URLWithString:key];
    NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
    // File system has file name length limit, we need to check if ext is too long, we don't add it to the filename
    if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) {
        ext = nil;
    }
    // 最終生成的文件名就是 "md5碼"+".文件類型"
    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]];
    return filename;
}
  • 解釋

1.key為圖片下載路徑,例如 https://imgs3.taobao/banners/201812/82f6bcdeb55191b75f3371056f093388.jpg

2.self.diskCachePath為存儲(chǔ)路徑,例如模擬器路徑

/Users/cs/Library/Developer/CoreSimulator/Devices/053EEA1A-F097-409F-A232-51E51A239304/data/Containers/Data/Application/69AA9017-C1CE-4FA4-8A81-253E6AB05CF9/Library/Caches/default/com.hackemist.SDWebImageCache.default

本文部分參考# SDWebImage緩存機(jī)制,非常感謝該作者。

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

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

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