@(Third-Library Set)[SDWebImage, SDImageCache, 缓存策略] #SDWebImage 缓存策略分析 [TOC]
##简介 SDWebImage
不仅仅提供了简单的图片下载操作,而且还提供了图片的缓存.比如: 内存缓存(memory cache)
磁盘缓存(disk cache)
. 还有对缓存的一些处理操作,比如: 按 缓存大小
,缓存存在时间
等多个方面来进行缓存的清理.
##图片缓存 ###缓存策略 SDImageCache
是SD
库中处理缓存的类.枚举类型SDImageCacheType
提供了三种缓存选择: SDImageCacheTypeNone
, SDImageCacheTypeDisk
, SDImageCacheTypeMemory
. 字面意思就是: 不做缓存, 磁盘缓存以及内存缓存. 默认是内存和磁盘都进行缓存.
缓存选项:
选项 | 作用 |
---|---|
shouldDecompressImages | 是否解压图片 |
shouldDisableiCloud | 是否备份到 iCloud |
shouldCacheImagesInMemory | 是否缓存到内存中 |
maxMemoryCost | 最大的内存占用 size |
maxMemoryCountLimit | 最大内存持有的对象个数 |
maxCacheAge | 缓存的最长时间 |
maxCacheSize | 最大的磁盘缓存 size |
###内存缓存 ####缓存类 内存缓存这块,主要是使用了NSCache
类, 因为NSCache
自身独有的一些性质,使得其较NSDictionary
有一些优势. NSCache
的官方文档有这样的介绍: A mutable collection you use to temporarily store transient key-value pairs that are subject to eviction when resources are low.
一个用于存储临时的键值对的可变集合,当内存资源不够时,存储的对象将会被收回. 这样一个自动被收回的机制对于缓存这样不是特别重要的数据来说,还是很不错的.
SD
中对NSCache
还做了简单的处理,我们看一下源码:
@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
@end
复制代码
SD
实现了一个NSCache
的子类,主要就是在初始化的方法中,又添加了一个监听事件,当收到内存警告的通知时执行了一个removeAllObjects
方法,不知道是不是作者遇到了,即便内存吃紧时,NSCache
也没有被系统回收的情况,所以在这里又做了一层保险呢? NSCache
还有一点需要注意的是: 当执行了removeAllObjects
方法,或者移除了其所有的元素后,就无法再添加元素到NSCache
中了. ####缓存时机
SDWebImageDownloader
中的请求图片方法的回调中,当图片请求成功后,把图片存储到内存缓存中. 即: 图片请求成功后存储到缓存中.- App启动后,同一个
URL
的图片已经被请求过,而且已经缓存到磁盘里了,这时SD
在请求图片前会根据图片的URL
去磁盘里查找,当找到磁盘中的图片后,再将图片加到内存缓存中. ###磁盘缓存 磁盘缓存和内存缓存的时机相似,当图片请求回来后,进行磁盘缓存.当然这个缓存会根据用户的设置,如果用户设置了SDWebImageCacheMemoryOnly
选项,那么就不会在磁盘中进行一份缓存了.
缓存操作使用NSFileManager
类,把URL
进行一个MD5
转换作为文件名称存储到指定的路径中. ##缓存处理 缓存很好的替我们解决了一张图片重复的发网络请求造成资源浪费的问题,也节省了用户的流量. 在内存中缓存的图片,在App被杀死后,会被收回,或者App在运行时,收到内存警告也会被收回.那么磁盘里面的缓存呢?如果不进行处理的话,日积月累,一些用不着的图片就成了垃圾,得不到清理,会占用用户手机很大的空间.
SD
也对这些缓存进行了处理. ###清理时机 我们需要了解一下SD
究竟都是在哪些时间点对缓存图片进行清理的呢? SDImageCache
类在初始化的时候,添加了三个监听:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
复制代码
可以归为三点:
- 收到内存就警告: 清理内存缓存
- App将要被杀死: 清理磁盘缓存
- App进入后台: 在后台清理磁盘缓存
无论按存储时间清理缓存
还是按缓存大小清理缓存
,都是在清理磁盘缓存时的操作. ###按存储时间清理缓存 像微博,朋友圈这样的应用,我们今天可能会刷一下昨天的动态,但是很少会刷一周前,甚至两周以前的动态.所以一两周以前缓存的图片的使用率是不高的,所以需要适时的进行清理.
SD
中默认的图片缓存时长为7天
.
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7;
复制代码
从现在开始算,往前推7天,得到一个NSDate
类型的对象:
//7天之前的日期.早于这个日期的文件,都属于应该被清理的对象.
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
//创建一个可变数组,将所有过期了的图片URL放到这个数组里面.
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
//存储非过期图片文件的总的大小,后面会用到.
NSUInteger currentCacheSize = 0;
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// Skip directories.
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
//获取图片文件最后的修改日期.
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
//如果修改日期早于过期日期.
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
//添加 url 到 urlsToDelete 数组中.
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
//计算未过期的图片文件的大小,并且累加到currentCacheSize中.
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
复制代码
上面的代码通过日期的比较,拿到了所有过期图片的URL
(这里的url是指图片在磁盘中的路径,以URL
的形式表示).
//遍历数组,挨个删除
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
复制代码
上面的代码就是把过期图片挨个删除掉的操作. 到这里就就已经把所有过期的文件给删除干净了. ###按缓存大小清理缓存 过期的图片是删除完了,可是剩下的这些未过期的图片,有可能占了100M,200M,对于16G手机内存的用户,仍然算是一个不小的开销,那么我们在软件中可能会设置一下最大的缓存Size,比如20M,或者50M.
那么未过期的图片文件,仍然远远超过了这些限制,这就需要我们针对缓存的Size,再一次对图片进行清理了.
//如果设置了最大缓存size,并且当前缓存的图片文件以及超过设置的size,那么进行缓存清理.
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
//将缓存的文件总的size降低到指定最大缓存size的一半.
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
//对文件进行一个排序,创建时间越早的图片放到数组前面.创建时间晚的图片放到数组后面,因为首要删除的是创建比较早的图片.
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
//开始删除文件
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
//当缓存的文件总的size小于我们期望的size后,停止删除.
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
复制代码
代码其实很简单,主要涉及到对文件的一些操作,比如获取文件的AllocatedSize
,获取文件的ModificationDate
等属性. 还有最后一个就是后台清理磁盘中的图片的操作,之前也有提到过,就是调用了一个UIApplication
类的方法: beginBackgroundTaskWithExpirationHandler:
,这个方法也不难,看一下文档就可以了,关键是要再调用一下: endBackgroundTask:
这个方法. 贴上代码:
- (void)backgroundCleanDisk {
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
// Clean up any unfinished task business by marking where you
// stopped or ending the task outright.
//这个block就是后台任务执行的时间即将要结束时,要调用下面这个方法来结束任务.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// Start the long-running task and return immediately.
[self cleanDiskWithCompletionBlock:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
复制代码
##总结
- 网络图片请求回来后会进行内存缓存和磁盘缓存
- 当本地磁盘内有已经缓存好的图片时,直接从磁盘中取图片,然后再缓存到内存中.
- 当系统内存吃紧时,内存缓存会被自动释放,释放后,就只能去取磁盘的缓存,无法再进行内存缓存,即便这样也会比网络请求要快很多而且节省资源.
- 存储到磁盘的缓存分别会在程序被杀死前和程序进入后台时进行一次磁盘缓存的清理.
- 提供了诸多种缓存配置选项,如: 是否备份到iCloud, 是否解压图片等等.
- 根据缓存的过期时间 和缓存的*最大 size *等限制进行图片的清理.
很多细节没有把握到,还请多多包涵,如有错误,还请不吝指正.
##参考 SDWebImage 3.8.2