SDWebImage缓存策略

SDWebImage包含了基本的图片下载、缓存、编解码性能优化等功能集成,具备较完善的图片处理流程、和较好的框架可扩展性,成为很多APP中图片加载功能的标配;这片文章主要介绍库的缓存处理部分,了解其缓存处理的细节;

在看具体的缓存处理前,先了解下关于这个库的功能模块组成,方便对库的整体结构有一个大概的了解;以下这张图包含了这个库的基本功能模块组成;可以看到为了实现比较完善的网络图片功能,SDWebImage拆分了不同的子模块来实现对应的功能子集;

组成结构.png

上图红线框中选中的部分包括下载、缓存与图片编解码处理,属于框架中最核心的部分;其他部分主要作用是对核心功能的扩展与支撑、和对图片处理流程的功能补充;以下是对核心组成模块的功能说明:

  • Manager:网络图片处理的调度器,负责把不同模块实现的图片处理流程串起来;
  • Downloader:内部通过一个NSOperationQueue队列管理图片的下载任务
  • Cache:负责图片(内存、磁盘)的缓存处理
  • Decoder:负责图片数据的编解码处理,优化图片加载性能

SDWebImage本身的代码并不复杂,主要在于其每一个模块都相对独立,模块的拆分和具体模块的代码设计都非常优秀;了解这个库的源代码除了能了解到我们日常使用的功能内部的实现细节外、还能借鉴到优秀的代码设计风格,提高自己的代码设计能力

这篇文章主要是对‘Cache’缓存处理部分的理解;下图是框架内缓存处理的UML设计类图,由此可以了解到缓存内部的类设计与组成、和具备的基本功能方法(不喜看图的可以略过,下文会对缓存的组成类做功能说明):

SDWebImageCacheClassDiagram.png

缓存相关类主要包括如下几个:

  • SDImageCache:图片的缓存调度入口,内部调用内存和磁盘的缓存类实现具体的缓存功能
  • SDImageCacheConfig:缓存的全局配置信息,可以通过这个类设置磁盘缓存过期时间、最大字节数
  • SDMemoryCache:内存缓存类,继承自系统的NSCache
  • SDDiskCache:磁盘缓存类,通过文件的形成储存单张图片数据
  • SDImageCacheDefine:定义了<SDImageCache>协议;体现了面向协议编程的优势,方便扩展,如果需要我们可以实现自己的缓存类-遵循该协议,替换掉框架默认的缓存实现
  • SDImageCachesManager:通常用不到,可以管理多个<SDImageCache>缓存对象共存的场景,大多数时候只需要一个Cache对象即可

SDWebImage关于Cache部分的处理主要包括,缓存的增、删、查、判断有无、及缓存的清理这几个部分;

这里涉及一个比较重要的概念,就是关联到每个缓存数据的key值,框架内部会使用传进来的图片名称作为原始数据,通过CC_MD5算法生成一个图片的唯一标识符,这个唯一标识符会作为缓存key值与具体的某一条缓存数据关联,通过这个key值可以执行增删查等操作;因此需要确保每一张缓存的图片名称是唯一的,相同的key图片数据会被覆盖;

当加载网络图片时,默认会把图片的URL字符串作为缓存key的唯一标识符,当然框架提供了cacheKeyFilter属性让我们设置自己的key标识规则;这个属性在某些场合很有用,比如当你的图片链接由于带一些时间戳或者签名等参数时,通常这样的参数有可能在每次请求图片时都不一样,每次同一个图片的链接发生变化导致我们原先的缓存失效 需要重新下载并缓存;这时就可以通过设置这个属性的自定义key规则,过滤掉无效的每次会变的参数部分,保证缓存可以正常的读写;

以下在深入看一下内存缓存部分磁盘缓存部分、和缓存的清理策略在框架内部的具体实现策略

内存缓存部分

SDMemoryCache类用来实现默认的内存缓存方式,这个类继承自NSCache,并遵循SDMemoryCache协议,实现了对应的协议方法;框架上声明了SDMemoryCache协议,并没有直接写死具体的缓存策略,这么做也是对面向协议编程的实践,未来可以更方便的对缓存策略做不同的实现扩展,或者留给用户实现自定义的缓存处理策略(只需要实现自己的类遵循SDMemoryCache协议,并实现具体的协议方法来处理自定义的缓存逻辑即可);这体现了面向对象编程所倡导的依赖反转原则;同理在磁盘缓存中也有类似的实践。

SDMemoryCache内部主要通过父类NSCache的方法来实现内存缓存数据的读与写,NSCache本身是线程安全的,缓存处理的方法也是在同步线程中完成的,会阻塞当前线程;这是由于内存缓存的访问本身并不耗时 即使同步访问也不会带来性能上的问题;这个类的私有属性主要包括以下几个:

@property (nonatomic, strong, nullable) SDImageCacheConfig *config;
#if SD_UIKIT
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache` thread-safe
@end

其中weakCache属性,会在内部对缓存数据做一个弱引用,其目的是在内存缓存被释放时(如应用退后台、或者收到内存警告等),如果当前页面有UI控件类似 UIImageView等页面显示的对象还持有了图片数据时,可以根据这个弱引用key缓存的数据重新把数据缓存回内存中;这样可以避免该数据后续读取的磁盘IO或者重新下载;

weakCacheLock属性,是一个GCD锁,保证了操作weakCache属性的线程安全性;

磁盘缓存部分

SDDiskCache类用来实现默认的磁盘缓存策略,内部主要通过一个NSFileManager对象来控制图片文件的读写操作;SDDiskCache也遵循了协议式编程,允许我们在必要时通过实现自定义的磁盘缓存类(需要遵循<SDDiskCache>协议),以替换掉框架默认的磁盘缓存策略;

SDDiskCache内部通过NSFileManager对象操作NSData类型的磁盘缓存数据的读写与删除方法;由于磁盘读写相对耗时,且读写操作需要满足线程安全;这个类中通过一个GCD的Serial类型的队列完成磁盘缓存数据的IO操作方法的调用,以确保多线程下的数据读写安全,同时保证不阻塞主线程的正常流程,避免带来性能问题;

这个类内部主要实现的是关于NSFileManager文件的读写操作,在接收缓存数据时,如果传入的是UIImage类型的数据,则会通过Decoder模块先对图片做编码操作,转化成NSData后再完成本地的文件存储调用;这里会对传递的缓存key值调用CC_MD5方法来组合生成一个唯一文件名,关联到这个缓存对象;

以下可以看下这个类中关于读写缓存数据的API原型:

/**
 Returns the data associated with a given key.
 This method may blocks the calling thread until file read finished
 */
- (nullable NSData *)dataForKey:(nonnull NSString *)key;

/**
 Sets the value of the specified key in the cache.
 This method may blocks the calling thread until file write finished.
 
 @param data The data to be stored in the cache.
 */
- (void)setData:(nullable NSData *)data forKey:(nonnull NSString *)key;

/**
 Removes the value of the specified key in the cache.
 This method may blocks the calling thread until file delete finished. 
 */
- (void)removeDataForKey:(nonnull NSString *)key;

/**
 Empties the cache.
 This method may blocks the calling thread until file delete finished.
 */
- (void)removeAllData;

/**
 Removes the expired data from the cache. You can choose the data to remove base on `ageLimit`, `countLimit` and `sizeLimit` options.
 */
- (void)removeExpiredData;

@end

缓存清理部分

首先SDWebImage允许我们根据指定的key值,来清理特定的换粗图片数据;只需要调用对应的API传入key值就可以做到;以下部分是关于全局的缓存清理策略与方法的说明

SDWebImage默认的缓存清理周期是一星期,当然框架上允许我们自己设置这个过期时间,通过设置SDImageCacheConfig缓存配置类的maxDiskAge属性即可;具体的缓存清理策略分为两种,如以下两个枚举值所示:

缓存清理策略.png

第一种是按图片的最后访问时间计时,超过设置的过期时间后被清理;
第二种是按图片的最后修改时间计时,例如重新下载设置了图片,已最后一次修改过图片的时间节点来计时,超过设置的过期时间后被清理

清理的动作会在应用程序退出时(applicationWillTerminate方法被调用) 执行一次;默认情况下应用退出到后台时(并未退出),也会执行一次检测清理的操作,不过这个检测开关可以由开发者来主动设置,默认是开启的;

以下是内存缓存磁盘缓存的缓存清理执行方法,通过这两个方法我们能更清晰的了解到缓存是如何被清理掉的:

///内存缓存清理方法
- (void)removeAllObjects {
    [super removeAllObjects];  //调用父类NSCache的方法,可以直接清理完内存缓存

    //判断是否有缓存的弱引用存在,如果有需要把弱引用对象一起清理掉
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    // Manually remove should also remove weak cache
    SD_LOCK(self.weakCacheLock);
    [self.weakCache removeAllObjects];
    SD_UNLOCK(self.weakCacheLock);
}
@end

可以看到内容的缓存清理方法实际上非常简单,只需要调用父类NSCache的 removeAllObjects方法就可以完成对缓存的清理任务;关于self.weakCacheLock对象的作用在内存缓存部分已做过说明,这里因为内存缓存已不再需要,需要一并把这部分弱引用一起清理掉;

///磁盘缓存清理方法

- (void)removeExpiredData {
    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;
    }
    
    NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
    
    // This enumerator prefetches useful properties for our cache files.
    NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                               includingPropertiesForKeys:resourceKeys
                                                                  options:NSDirectoryEnumerationSkipsHiddenFiles
                                                             errorHandler:NULL];
    
    NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
    NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
    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.
        if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
            continue;
        }
        
        // Remove files that are older than the expiration date;
        NSDate *modifiedDate = resourceValues[cacheContentDateKey];
        if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
            [urlsToDelete addObject:fileURL];
            continue;
        }
        
        // Store a reference to this file and account for its total size.
        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
        currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
        cacheFiles[fileURL] = resourceValues;
    }
    
    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.
    NSUInteger maxDiskSize = self.config.maxDiskSize;
    if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
        // Target half of our maximum cache size for this cleanup pass.
        const NSUInteger desiredCacheSize = maxDiskSize / 2;
        
        // Sort the remaining cache files by their last modification time or last access time (oldest first).
        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.
        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;
                
                if (currentCacheSize < desiredCacheSize) {
                    break;
                }
            }
        }
    }
}

单个数据的磁盘缓存清理非常简单,只要根据对应的key检索到文件,并判断文件存在,直接调用NSFileManagerremoveItemAtPath方法即可完成清理;

以上这个方法是关于如何清理过期的(包括超过设置的总缓存大小的部分)磁盘缓存数据的,总结起来过程如下:

  • 获取出所有的缓存对象,首先清理掉过期的缓存对象;
  • 计算出未过期的缓存大小总和,并与最大的缓存空间的 1/2 作对比;
  • 按时间最久未被访问(或修改)的顺序,对缓存数据做一个排序;
  • 根据排序结果继续清理 时间最久远的缓存图片数据,直到剩余的缓存大小 小于最大空间的1/2时,停止清理;

以上就是SDWebImage这个网络库关于缓存部分的大致实现原理;通过这篇文档能了解到SDWebImage大致组成结构和其内部的缓存处理实现;这个图片库本身的代码并不复杂,最优秀的地方应该体现在其代码的结构设计上;

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

推荐阅读更多精彩内容