SDWebImage 解析之 SDImageCache

這里會做一些源碼的解析,也會對使用較多的類或方法進行簡單的介紹!
目前為止sdwebimage已經更新到4.0.0 測試的第二個版本,我們拿到源碼來看看。
只要修改源碼的同志們記得吃藥,大的架構永遠不會變。

一. SDImageCache (sdwebimage緩存類)

我們先來看看SDImageCache.h文件中的內容

#import <Foundation/Foundation.h>
#import "SDWebImageCompat.h"

@class SDImageCacheConfig;

typedef NS_ENUM(NSInteger, SDImageCacheType) {
/**
 * The image wasn't available the SDWebImage caches, but was downloaded from the web.
 */
//不用緩存
SDImageCacheTypeNone,
/**
 * The image was obtained from the disk cache.
 */
//磁盤緩存
SDImageCacheTypeDisk,
/**
 * The image was obtained from the memory cache.
 */
//內存緩存
SDImageCacheTypeMemory
};

typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);

typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);

typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);


/**
 * SDImageCache maintains a memory cache and an optional disk  cache. Disk cache write operations are performed
 * asynchronous so it doesn’t add unnecessary latency to the UI.
 * SDImageCache類用于操控內存緩存和可選磁盤緩存。 磁盤高速緩存寫入操作是異步執(zhí)行的,因此不會對UI增加不必要的延遲
 */
@interface SDImageCache : NSObject

#pragma mark - Properties

/**
 *  Cache Config object - storing all kind of settings
 *  默認:解壓縮,但是會消耗很大內存,如果程序崩潰,設為NO
 *  默認:禁用iCloud備份
 *  默認:最長緩存時間 單位:秒  一周
 *  默認:內存緩存
 */
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;

/**
 * The maximum "total cost" of the in-memory image cache. The cost function is the number of pixels held in memory.
 * 內存中圖像緩存的最大“總成本”。 成本函數是存儲器中保存的像素數
 */
@property (assign, nonatomic) NSUInteger maxMemoryCost;

/**
 * The maximum number of objects the cache should hold.
 * 緩存應該保留的對象的最大數量
 */
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;

#pragma mark - Singleton and initialization

/**
 * Returns global shared cache instance
 *
 * @return SDImageCache global instance
 */
//單例
+ (nonnull instancetype)sharedImageCache;

/**
 * Init a new cache store with a specific namespace
 *
 * @param ns The namespace to use for this cache store
 */
//同下
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;

 /**
  * Init a new cache store with a specific namespace and directory
 *
 * @param ns        The namespace to use for this cache store
 * @param directory Directory to cache disk images in
 */
//初始化,執(zhí)行此方法會根據ns及directory生成緩存在磁盤中的路徑diskCachePath
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                   diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;

#pragma mark - Cache paths
//同上,生成diskCachePath(兩者又是不同的,具體,看.m文件區(qū)別)
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace;

/**
 * Add a read-only cache path to search for images pre-cached by SDImageCache
 * Useful if you want to bundle pre-loaded images with your app
 *
 * @param path The path to use for this read-only cache path
 */
- (void)addReadOnlyCachePath:(nonnull NSString *)path;

#pragma mark - Store Ops

/**
 * Asynchronously store an image into memory and disk cache at the given key.
 *
 * @param image           The image to store
 * @param key             The unique image cache key, usually it's image absolute URL
 * @param completionBlock A block executed after the operation is finished
 */
//根據key緩存圖片(通常情況下key是image absolute URL)
- (void)storeImage:(nullable UIImage *)image
        forKey:(nullable NSString *)key
    completion:(nullable SDWebImageNoParamsBlock)completionBlock;

/**
 * Asynchronously store an image into memory and disk cache at the given key.
 *
 * @param image           The image to store
 * @param key             The unique image cache key, usually it's image absolute URL
 * @param toDisk          Store the image to disk cache if YES
 * @param completionBlock A block executed after the operation is finished
 */
//同上,指示是否寫入磁盤
- (void)storeImage:(nullable UIImage *)image
        forKey:(nullable NSString *)key
        toDisk:(BOOL)toDisk
    completion:(nullable SDWebImageNoParamsBlock)completionBlock;

/**
 * Asynchronously store an image into memory and disk cache at the given key.
 *
 * @param image           The image to store
 * @param imageData       The image data as returned by the server, this representation will be used for disk storage
 *                        instead of converting the given image object into a storable/compressed image format in order
 *                        to save quality and CPU
 * @param key             The unique image cache key, usually it's image absolute URL
 * @param toDisk          Store the image to disk cache if YES
 * @param completionBlock A block executed after the operation is finished
 */
//同上 imagedata服務器返回的未解壓的數據
- (void)storeImage:(nullable UIImage *)image
     imageData:(nullable NSData *)imageData
        forKey:(nullable NSString *)key
        toDisk:(BOOL)toDisk
    completion:(nullable SDWebImageNoParamsBlock)completionBlock;

/**
 * Synchronously store image NSData into disk cache at the given key.
 *
 * @warning This method is synchronous, make sure to call it from the ioQueue
 *
 * @param imageData  The image data to store
 * @param key        The unique image cache key, usually it's image absolute URL
 */
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;

#pragma mark - Query and Retrieve Ops

/**
 *  Async check if image exists in disk cache already (does not load the image)
 *
 *  @param key             the key describing the url
 *  @param completionBlock the block to be executed when the check is done.
 *  @note the completion block will be always executed on the main queue
 */
//在磁盤中查找是否有key對應的image對象,block中是一bool值
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

/**
 * Operation that queries the cache asynchronously and call the completion when done.
 *
 * @param key       The unique key used to store the wanted image
 * @param doneBlock The completion block. Will not get called if the operation is cancelled
 *
 * @return a NSOperation instance containing the cache op
 */
//先在memory中查找,找不到去disk查找(sdwebimageManager的回調方法)
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;

/**
 * Query the memory cache synchronously.
 *
 * @param key The unique key used to store the image
 */
//查找內存中key對應的圖片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;

/**
 * Query the disk cache synchronously.
 *
 * @param key The unique key used to store the image
 */
//查找磁盤中key對應的圖片
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;

/**
 * Query the cache (memory and or disk) synchronously after checking the memory cache.
 *
 * @param key The unique key used to store the image
 */
//這個注釋什么意思?after checking the memory cache?
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;

#pragma mark - Remove Ops

/**
 * Remove the image from memory and disk cache asynchronously
 *
 * @param key             The unique image cache key
 * @param completion      A block that should be executed after the image has been removed (optional)
 */
//內存、磁盤都刪
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;

/**
 * Remove the image from memory and optionally disk cache asynchronously
 *
 * @param key             The unique image cache key
 * @param fromDisk        Also remove cache entry from disk if YES
 * @param completion      A block that should be executed after the image has been removed (optional)
 */
//可選磁盤中的文件是否刪除
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

#pragma mark - Cache clean Ops

/**
 * Clear all memory cached images
 */
//清空內存
- (void)clearMemory;

/**
 * Async clear all disk cached images. Non-blocking method - returns immediately.
 * @param completion    A block that should be executed after cache expiration completes (optional)
 */
//清理磁盤中的所有文件
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

/**
 * Async remove all expired cached image from disk. Non-blocking method - returns immediately.
 * @param completionBlock A block that should be executed after cache expiration completes (optional)
 */
//清除過期文件
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

#pragma mark - Cache Info

/**
 * Get the size used by the disk cache
 */
//獲取緩存文件總大小
 - (NSUInteger)getSize;

/**
 * Get the number of images in the disk cache
 */
//獲取緩存文件數量
- (NSUInteger)getDiskCount;

/**
 * Asynchronously calculate the disk cache's size.
 */
//計算緩存總大小
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock;

#pragma mark - Cache Paths

/**
 *  Get the cache path for a certain key (needs the cache path root folder)
 *
 *  @param key  the key (can be obtained from url using cacheKeyForURL)
 *  @param path the cache path root folder
 *
 *  @return the cache path
 */
//key經過md5之后生成一個字符串拼接在path后面生成完整路徑
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path;

/**
 *  Get the default cache path for a certain key
 *
 *  @param key the key (can be obtained from url using cacheKeyForURL)
 *
 *  @return the default cache path
 */
//同上
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key;

@end


下面,我們來看看SDImageCache.m中的方法實現

- (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 = SDCacheCostForImage(image);
    [self.memCache setObject:image forKey:key cost:cost];
}

if (toDisk) {
    dispatch_async(self.ioQueue, ^{
        NSData *data = imageData;
        //image存在,但是其對應的NSData不存在,則重新生成data
        if (!data && image) {
            //判斷圖片類型,然后將圖片轉成data(詳見NSData+ImageContentType和UIImage+MultiFormat文件)
            SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
            data = [image sd_imageDataAsFormat:imageFormatFromData];
        }
        //存儲到磁盤
        [self storeImageDataToDisk:data forKey:key];
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
} else {
    if (completionBlock) {
        completionBlock();
    }
  }
}

- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
    return;
}

[self checkIfQueueIsIOQueue];
/*
 * 獲取到需要存儲的data后,使用fileManager進行存儲
 * 首先判斷disk cache的文件路徑是否存在,不存在的話就創(chuàng)建一個
 * disk cache的文件路徑是存儲在_diskCachePath中的
 */
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}

// get cache Path for image key

/*
 * 根據image的key(一般情況下理解為image的url)組合成最終的文件路徑
 * 上面那個生成的文件路徑只是一個文件目錄,就跟/cache/images/img1.png和cache/images/的區(qū)別一樣
 */
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl

/*
 * 這個url可不是網絡端的url,而是file在系統(tǒng)路徑下的url
 * 比如/foo/bar/baz --------> file:///foo/bar/baz
 */
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//根據存儲的路徑(cachePathForKey)和存儲的數據(data)將其存放到iOS的文件系統(tǒng)
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];

// disable iCloud backup(icould不備份)
if (self.config.shouldDisableiCloud) {
    [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
  }
}

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
    /*
     * 這兩個變量主要是為了下面生成NSDirectoryEnumerator準備的
     * 一個是記錄遍歷的文件目錄,一個是記錄遍歷需要預先獲取文件的哪些屬性
     */
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

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

    /*
     * 獲取文件的過期時間,SDWebImage中默認是一個星期
     * 不過這里雖然稱*expirationDate為過期時間,但是實質上并不是這樣
     * 其實是這樣的,比如在2016/12/12/00:00:00最后一次修改文件,對應的過期時間應該是
     * 2016/12/19/00:00:00,不過現在時間是2016/12/27/00:00:00,我先將當前時間減去1個星期,得到
     * 2016/12/20/00:00:00,這個時間才是我們函數中的expirationDate
     * 用這個expirationDate和最后一次修改時間modificationDate比較看誰更晚就行
     */
    NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
    //用來存儲對應文件的一些屬性,比如文件所需磁盤空間
    NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
    //記錄當前已經使用的磁盤緩存大小
    NSUInteger currentCacheSize = 0;

    // 在緩存的目錄開始遍歷文件.  此次遍歷有兩個目的:
    //  1. 移除過期的文件
    //  2. 同時存儲每個文件的屬性(比如該file是否是文件夾、該file所需磁盤大小,修改時間)
    NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
        NSError *error;
        NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

        // 當前掃描的是目錄,就跳過
        if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
            continue;
        }

        // 移除過期文件(這里判斷過期的方式:對比文件的最后一次修改日期和expirationDate誰更晚,如果expirationDate更晚,就認為該文件已經過期,具體解釋見上面)
        NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
        if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
            [urlsToDelete addObject:fileURL];
            continue;
        }

        // 計算當前已經使用的cache大小,并將對應file的屬性存到cacheFiles中
        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
        currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
        cacheFiles[fileURL] = resourceValues;
    }
    
    for (NSURL *fileURL in urlsToDelete) {
        // 根據需要移除文件的url來移除對應file
        [_fileManager removeItemAtURL:fileURL error:nil];
    }

    // 如果我們當前cache的大小已經超過了允許配置的緩存大小,那就刪除已經緩存的文件
    // 刪除策略就是,首先刪除修改時間更早的緩存文件
    if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
        // 直接將當前cache大小降到允許最大的cache大小的一般
        const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

        // 根據文件修改時間來給所有緩存文件排序,按照修改時間越早越在前的規(guī)則排序
        NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                 usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                     return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                 }];

        // 每次刪除file后,就計算此時的cache的大小.
        //如果此時的cache大小已經降到期望的大小了,就停止刪除文件了
        for (NSURL *fileURL in sortedFiles) {
            if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                // 獲取該文件對應的屬性
                NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                // 根據resourceValues獲取該文件所需磁盤空間大小
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                // 計算當前cache大小
                currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;

                if (currentCacheSize < desiredCacheSize) {
                    break;
                }
            }
        }
    }
    // 如果有completionBlock,就在主線程中調用
    if (completionBlock) {
        dispatch_async(dispatch_get_main_queue(), ^{
            completionBlock();
        });
    }
});
}

- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
  const char *str = key.UTF8String;
  if (str == NULL) {
      str = "";
  }
  /*
   * 使用了MD5進行加密處理
   * 開辟一個16字節(jié)(128位:md5加密出來就是128bit)的空間
   */
  unsigned char r[CC_MD5_DIGEST_LENGTH];
  /*
   * 官方封裝好的加密方法
   * 把str字符串轉換成了32位的16進制數列(這個過程不可逆轉) 存儲到了r這個空間中
   */
  CC_MD5(str, (CC_LONG)strlen(str), r);
  // 最終生成的文件名就是 "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], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];

  return filename;
}

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容