SDWebImage
包含了基本的图片下载、缓存、编解码性能优化等功能集成,具备较完善的图片处理流程、和较好的框架可扩展性,成为很多APP中图片加载功能的标配;这片文章主要介绍库的缓存处理部分,了解其缓存处理的细节;
在看具体的缓存处理前,先了解下关于这个库的功能模块组成,方便对库的整体结构有一个大概的了解;以下这张图包含了这个库的基本功能模块组成;可以看到为了实现比较完善的网络图片功能,SDWebImage
拆分了不同的子模块来实现对应的功能子集;
上图红线框中选中的部分包括下载、缓存与图片编解码处理,属于框架中最核心的部分;其他部分主要作用是对核心功能的扩展与支撑、和对图片处理流程的功能补充;以下是对核心组成模块的功能说明:
- Manager:网络图片处理的调度器,负责把不同模块实现的图片处理流程串起来;
- Downloader:内部通过一个NSOperationQueue队列管理图片的下载任务
- Cache:负责图片(内存、磁盘)的缓存处理
- Decoder:负责图片数据的编解码处理,优化图片加载性能
SDWebImage
本身的代码并不复杂,主要在于其每一个模块都相对独立,模块的拆分和具体模块的代码设计都非常优秀;了解这个库的源代码除了能了解到我们日常使用的功能内部的实现细节外、还能借鉴到优秀的代码设计风格,提高自己的代码设计能力
这篇文章主要是对‘Cache’缓存处理部分的理解;下图是框架内缓存处理的UML设计类图,由此可以了解到缓存内部的类设计与组成、和具备的基本功能方法(不喜看图的可以略过,下文会对缓存的组成类做功能说明):
缓存相关类主要包括如下几个:
- 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
属性即可;具体的缓存清理策略分为两种,如以下两个枚举值所示:
第一种是按图片的最后访问时间计时,超过设置的过期时间后被清理;
第二种是按图片的最后修改时间计时,例如重新下载设置了图片,已最后一次修改过图片的时间节点来计时,超过设置的过期时间后被清理
清理的动作会在应用程序退出时(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检索到文件,并判断文件存在,直接调用NSFileManager
的removeItemAtPath
方法即可完成清理;
以上这个方法是关于如何清理过期的(包括超过设置的总缓存大小的部分)磁盘缓存数据的,总结起来过程如下:
- 获取出所有的缓存对象,首先清理掉过期的缓存对象;
- 计算出未过期的缓存大小总和,并与最大的缓存空间的 1/2 作对比;
- 按时间最久未被访问(或修改)的顺序,对缓存数据做一个排序;
- 根据排序结果继续清理 时间最久远的缓存图片数据,直到剩余的缓存大小 小于最大空间的1/2时,停止清理;
以上就是SDWebImage
这个网络库关于缓存部分的大致实现原理;通过这篇文档能了解到SDWebImage
大致组成结构和其内部的缓存处理实现;这个图片库本身的代码并不复杂,最优秀的地方应该体现在其代码的结构设计上;