SDImageCache 的主要属性和方法都在第一篇的使用中介绍过了,下面主要讲解详细实现
缓存过程
因为网络速度和流量的考虑
SDWebImage 将下载的图片进行内存和磁盘缓存
- 内存缓存保证读取缓存的速度
- 磁盘缓存空间大,可以缓存的大量的图片
保存时先将下载的图片存入内存缓存,然后存入磁盘缓存,
读取时先从内存缓存中读取,如果不存在,再去磁盘中读取缓存,
节省流量,图片加载时间,提升用户体验
内存缓存使用 NSCache
NSCache 只有如下方法
使用方法类似 NSDictionary 只需设置 NSCache 能占用的最大内存totalCostLimit或者最多缓存数量countLimit,然后将需要缓存的图片,对象等 setValue:forKey:cost即可
比如我们设置缓存最多占用20mb,然后每次存入缓存图片时将图片大小作为 cost 参数传入,
当缓存大小或数量超过限定值时,内部的缓存机制就会自动为我们执行清理操作
而且NSCache 是线程安全的.
但是 SDWebImage 并不是这样使用的, 并没有设置缓存可以占用的最大内存量,也没有设置最大可缓存的对象数量
// See https://github.com/rs/SDWebImage/pull/1141 for discussion
@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
SDWebImage 自定义了一个自动清理的缓存,监听 UIApplicationDidReceiveMemoryWarningNotification 通知,来清理缓存
我们仍可以主动设置 SDWebImageCache的
NSUInteger maxMemoryCost //缓存最多能占用多少内存,默认是0,无限大
NSUInteger maxMemoryCountLimit //最多能缓存多少张图片
来限制 SDWebImage 的内存占用
磁盘缓存使用 NSFileManager
在沙盒的Dictionary中,建立 com.hackemist.SDWebImageCache.default 目录,将每一个下载完成的图片存储为一个单独文件,文件名为根据图片对应的 Url用 MD5加密生成的字符串,类似 1d067b6f4457574b8165aef42643752e,这个字符串在 App 内唯一
ioQueue
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);//串行队列,队列里的一个任务执行完毕才执行下一个
磁盘缓存操作都在这个队列里异步执行,因为它是串行队列,任务一个执行完毕才执行下一个,所以不会出现一个文件同时被读取和写入的情况, 所以用 dispatch_async 而不必使用 disathc_barrier_async
缓存图片策略
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
1.先存入内存缓存
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
2.在 ioQueue 中串行处理所有磁盘缓存,
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
if (data) {
3.创建放缓存文件的文件夹
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
4.根据 image 的 远程url 生成本地缓存图片对应的 url
先将远程的 url 进行 md5加密,作为文件名,然后拼接到默认的缓存路径下,作为缓存文件的 url
com.hackemist.SDWebImageCache.default/1d067b6f4457574b8165aef42643752e
// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
5.将图片在磁盘中以文件的形式缓存起来,创建一个文件,写入 image 的 data
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
6. 防止 icloud 备份缓存
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
});
}
}
取出缓存图片的策略
取出缓存
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
1. 先搜索内存缓存
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
return image;
}
2.再搜索磁盘缓存
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
3.如果磁盘缓存中存在,将缓存图片放入内存缓存,并返回它
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
return diskImage;
}
取出内存缓存
//像 NSDictionary 一样,传入键,获取内存缓存的 image
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
取出磁盘缓存
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
1.根据图片的远程 url 生成本地缓存文件的 url, 根据 url 获取图片的 data
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
2.我们可以自定义缓存文件的存放路径,在自定义路径中搜索图片缓存
NSArray *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
if (imageData) {
return imageData;
}
}
return nil;
}
获取磁盘缓存大小
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock {
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
dispatch_async(self.ioQueue, ^{
NSUInteger fileCount = 0;
NSUInteger totalSize = 0;
1.遍历缓存目录下的所有文件
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:@[NSFileSize]
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
2.累加所有缓存文件的大小
for (NSURL *fileURL in fileEnumerator) {
NSNumber *fileSize;
[fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
totalSize += [fileSize unsignedIntegerValue];
fileCount += 1;
}
3.主线程中回调
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(fileCount, totalSize);
});
}
});
}
清除缓存
清除缓存的方式非常简单,删掉缓存目录,再重新创建一个即可,这会删掉 App 的所有缓存
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
{
dispatch_async(self.ioQueue, ^{
[_fileManager removeItemAtPath:self.diskCachePath error:nil];
[_fileManager createDirectoryAtPath:self.diskCachePath
withIntermediateDirectories:YES
attributes:nil
error:NULL];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
}
清理缓存,会删掉过期的缓存,根据 LRU (最近最少使用)算法,删除不常用的部分缓存
如果我们设置了 磁盘缓存最大占用空间 maxCacheSize, 那么清理缓存会保证磁盘缓存大小 < maxCacheSize / 2
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
1.遍历缓存目录下的所有缓存文件
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
2.删除所有过期的缓存文件
3.存储缓存文件的大小,为接下来 清理缓存防止其占用过大的磁盘空间,做准备
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
4.跳过文件夹
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
5.记录过期的缓存,一会一起删除
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
6.记录缓存文件的大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
7.删除过期的缓存
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
8.如果设置了缓存最大可占用的磁盘空间 self.maxCacheSize,那么接下来进行第二轮清理,防止缓存过大
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
9.根据修改时间排序缓存文件,
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
10.删除最旧的缓存文件,直到缓存文件大小 < 我们设定的 self.maxCacheSize /2
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
11.主线程回调
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
注意
默认情况下, SDWebImage已经监听广播来自动为我们执行清理操作
- 当收到内存警告时,清空内存缓存
- 当 App 进入关闭或进入后台时,清理磁盘缓存
[[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];