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类用于操控内存缓存和可选磁盘缓存。 磁盘高速缓存写入操作是异步执行的,因此不会对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
 */
//初始化,执行此方法会根据ns及directory生成缓存在磁盘中的路径diskCachePath
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                   diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;

#pragma mark - Cache paths
//同上,生成diskCachePath(两者又是不同的,具体,看.m文件区别)
- (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的文件路径是否存在,不存在的话就创建一个
 * 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/的区别一样
 */
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl

/*
 * 这个url可不是网络端的url,而是file在系统路径下的url
 * 比如/foo/bar/baz --------> file:///foo/bar/baz
 */
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//根据存储的路径(cachePathForKey)和存储的数据(data)将其存放到iOS的文件系统
[_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;

        // 根据文件修改时间来给所有缓存文件排序,按照修改时间越早越在前的规则排序
        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字节(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;
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容