SDWebImage 和 AFNetworking 中图片缓存策略的讨论

我们知道在 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 请求使用网络协议中实现的缓存逻辑,通过响应首部的一些信息透明的管理缓存,与我们上面所讨论的缓存机制十分不同。

你可能感兴趣的:(SDWebImage 和 AFNetworking 中图片缓存策略的讨论)