自定义SDWebImage图片缓冲区自清理机制
原创
©著作权归作者所有:来自51CTO博客作者拔山河的原创作品,请联系作者获取转载授权,否则将追究法律责任
SDWebImage是一个开源的第三方库,使用AFNetworking集成的UIImageView+AFNetworking.h,它对于图片的缓存实际应用的是NSURLCache自带的cache机制。NSURLCache每次都要把缓存的raw data 再转化为UIImage。
对它稍微改变可以进行三种缓存图片清理方式:
1.超时图片缓冲区的清理机制。它本身只包含默认七天超时的图片缓存机制,这个超时时间可以修改。
2.图片缓冲区大小清理机制,它默认没有启用,这个最大缓冲区大小可以修改。
3.图片缓冲区手动清理,它默认不具备,可以增加这个功能。
注意:只有你使用了它加载过图像这个图片缓冲区清除机制才生效。应用启动后没有用它加载过图片,它就没有被实例化所以也不会自动清理。
它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能
• 提供UIImageView的一个分类,以支持网络图片的加载与缓存管理
• 一个异步的图片加载器
• 一个异步的内存+磁盘图片缓存,并具有自动缓存过期处理功能
• 支持GIF图片
• 支持WebP图片
• 后台图片解压缩处理
• 确保同一个URL的图片不被下载多次
• 确保虚假的URL不会被反复加载
• 确保下载及缓存时,主线程不被阻塞
• 优良的性能
• 使用GCD和ARC
• 支持Arm64
app事件注册使用经典的观察者模式,当观察到内存警告、程序被终止、程序进入后台这些事件时,程序将自动调用相应的方法处理。
可以看到最常用的缓存文件被清理的时机时程序进入后台,一般的应用不会不进入后台就出现内存不足了吧!绝大部分使用者都是过一段时间会把应用切换到后台或杀掉,若是让应用切换到前台你的手机电量也吃不消。一般的应用也不会在七天内下载很大容量的图片。当然当你的手机已经产生了内存告警也能产生缓存图片清除。所以它的这种清除缓存图片的时机足够满足你的缓存图片处理。
注意:它默认只支持超过7天的图片清除。不对图片缓存大小进行控制。当然它已经做了这种机制,只是maxCacheSize为默认值0,所以不生效。
当然若你的应用下载的都是不压缩的大文件,如医学图像文件。那么你就要对图片缓冲区进行控制了,你可以修改下这个库。若你的应用有可能出现在亮屏幕的情况下就下载大量图片,并且图片整体很大,那么你也可以增加你清除图片的时机,主动调用图片缓存清理。当然你觉得7天时间太长需要修改超时时间,那么你也可以修改下这个库。最好的修改方法是对SDImageCache生成一个子类,来设置缓冲区的大小,清空图片缓冲区,当然你要把该类的对象存起来了,比直接修改这个库稍微麻烦下。
若你想直接修改第三方库来实现对缓冲区图片文件总大小控制或过期时间修改,只需要修改SDImageCache.m文件的3行代码就可以。若你想主动清理图片缓冲区,稍微修改下这个第三方库就可以。代码注释已经明确说明了:
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
//若想对图片缓冲区大小进行控制,请打开这个一行注释和后面用到该变量的地方的注释。这个是非本库的内容,若用pods重新下载后,若想使用该功能请加入相关的两行代码
//static const NSInteger kDefaultCacheMaxCacheSize = 100 * 1024 * 1024;//100 * 1024 * 1024; // 100 MB
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// 初始化PNG标记数据
kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
// 创建ioQueue串行队列负责对硬盘的读写
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// 初始化默认的最大缓存时间
_maxCacheAge = kDefaultCacheMaxCacheAge;
// //若想对图片缓冲区大小进行控制,请打开这个一行注释和前面用到该变量声明的地方的注释。这个是非本库的内容,若用pods重新下载后,若想使用该功能请加入相关的两行代码
// _maxCacheSize = kDefaultCacheMaxCacheSize;
// 初始化内存缓存,详见接下来解析的内存缓存类
_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;
// 初始化磁盘缓存
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
// 设置默认解压缩图片
_shouldDecompressImages = YES;
// 设置默认开启内存缓存
_shouldCacheImagesInMemory = YES;
// 设置默认不使用iCloud
_shouldDisableiCloud = YES;
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if TARGET_OS_IOS
// app事件注册,内存警告事件,程序被终止事件,已经进入后台模式事件,详见后文的解析: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];
//若想用户主动清理图片缓冲区,请打开这个注释。这个是非本库的内容,若用pods重新下载后,若想使用该功能请加入该代码,保证代码覆盖后的重新加入
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(cleanDisk)
// name:@"UserClenDiskNotification"
// object:nil]; #endif
}
return self;
}
如想在指定的地方主动清理缓存,打开以上通知相关代码。加入一行代码就可以了。注意,若是只有你使用过该库下载过图片,那么由于这个库才有对应监听者,才能清理图片缓冲区,否则你发出的清理图片缓冲区无效。
[[NSNotificationCenter defaultCenter] postNotificationName:@"UserClenDiskNotification";
下面是它的清除图片缓冲的核心代码,不需要修改,你了解一下就可以了。
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 使用目录枚举器获取缓存文件的三个重要属性:(1)URL是否为目录;(2)内容最后更新日期;(3)文件总的分配大小。
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
// 计算过期日期,默认为一星期前的缓存文件认为是过期的。
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 枚举缓存目录的所有文件,此循环有两个目的:
//
// 1. 清除超过过期日期的文件。
// 2. 为以大小为基础的第二轮清除保存文件属性。 NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// 跳过目录.
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 记录超过过期日期的文件;
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 保存保留下来的文件的引用并计算文件总的大小。
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
//清除记录的过期缓存文件
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果我们保留下来的磁盘缓存文件仍然超过了配置的最大大小,那么进行第二轮以大小为基础的清除。我们首先删除最老的文件。前提是我们设置了最大缓存
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// 此轮清除的目标是最大缓存的一半。
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// 用它们最后更新时间排序保留下来的缓存文件(最老的最先被清除)。
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 删除文件,直到我们达到期望的总的缓存大小。
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;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}