我们知道在 SDWebImage 中采取了二级缓存,先用 NSCache 做内存缓存,然后是磁盘缓存。我们先来看看NSCache是什么。
NSCache
NSCache 是一个类似于可变字典的集合类,也采用键值对存储值,但是它会通过自动释放其中的一些对象,帮我们做内存管理。Cache 内部根据总容量和对象个数来实现添加、删除、释放的策略。
NSCache 与可变的集合类有这几点不同:
- 为了确保一个 Cache 不会过多的占用系统内存,NSCache 合并了一些自动释放策略,如果其他应用需要内存,这些策略将会自动移除 Cache 中的对象,减少内存的占用。
- NSCache 是线程安全的,你可以在不同的线程中对同一 Cache 做添加、移除、查询操作
- 不像 NSMutableDictionary ,NSCache 中的键不需要实现 NSCopying 协议,键对象不会被复制。
对于一些创造较为耗时的对象,你可以利用 NSCache 暂时的存储它,但是,这些对象对于应用来说,并不是至关重要的,因为在内存紧张的时候,会自动被丢弃。
NSPurgeableData 是一个 NSData 对象,可将它标记为当前正在使用或者可清除,若把它保存到 NSCache 对象中并使用 endContentAccess 将其标记为可清除,iOS 在遇到内存压力时,会丢弃这些数据。
在 SDWebImage 中,声明了一个 NSCache 的子类,叫AutoPurgeCache ,它在初始化时,监听UIApplicationDidReceiveMemoryWarningNotification 通知,在收到内存警告时,会自动移除其中的所有对象,缓解内存压力。
@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
但是,采用 NSCache 的缓存机制,有一个不可避免的问题,何时释放其中的对象,并不是由我们来控制的,如何解决这个问题呢?
AFAutoPurgingImageCache
在 AFNetworking3.0 中,AFImageDownloader 默认采用的缓存为 AFAutoPurgingImageCache,这个类并不是 NSCache 的子类,而是一个记录了总存储容量、清空时优先保存的容量、当前已使用的容量的类。在每次添加图片时,都会计算容量限制,动态的清除一部分内存。
实现这个缓存机制之前,AFNetworking 先对 Image 做了一层封装,声明了一个叫做 AFCachedImage 的类,其中 lastAccessDate 属性记录了这张图片的最后访问时间,在初始化和访问图片时,都会更新这个时间,保持最新。
@interface AFCachedImage : NSObject
///封装的图片
@property (nonatomic, strong) UIImage *image;
///图片总大小
@property (nonatomic, assign) UInt64 totalBytes;
///最后访问时间,作为图片移除时的重要依据
@property (nonatomic, strong) NSDate *lastAccessDate;
@end
在 AFAutoPurgingImageCache 中,每一次增加图片之后,都会计算当前容量有没有超过 Cache 的总容量,若是超过了,利用 AFCachedImage 的 lastAccessDate 排序,首先删除最后访问时间比较早的,也就是最近没有访问过的图片,代码如下:
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
dispatch_barrier_async(self.synchronizationQueue, ^{
//生成标识为 identifier 的 AFCachedImage
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
//先检查有没有标识同样为 identifier 的图片,有的话,用新的图片替代较早的图片,并且重新计算当前容量
AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}
self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});
//超过最大容量时的处理
dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
//计算需要清除的容量
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
//然后将图片放置在一个数组中
NSMutableArray *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
//按照 lastAccessDate 升序排序
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];
//遍历 cache 中的图片,当移除图片的容量大于应该移除的容量时停止
UInt64 bytesPurged = 0;
for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});
}
因为对最后访问时间做升序排序,那么最早被访问的图片也是最早被清除掉的,实现了对 Cache 容量较为科学的控制。
NSURLCache
另外,在 AFImageDownloader 中,还有这么一个跟 NSCache 长得很像,但作用完全不同的类,叫 NSURLCache。
它实现了对 URL 请求的缓存,当收到服务器回应时,这个回应将在本地保存,同样的请求发出时,本地保存的回应将返回,减少了向服务器请求的次数。其内部采用了内存缓存和磁盘缓存两种机制,初始化时可以设置其内存、磁盘缓存大小以及磁盘路径。
AFImageDownloader 中为默认的 NSURLSessionConfiguration 设置 URLCache,并且设置缓存策略为对特定的 URL 请求使用网络协议中实现的缓存逻辑,通过响应首部的一些信息透明的管理缓存,与我们上面所讨论的缓存机制十分不同。