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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,175评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,674评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,151评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,597评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,505评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,969评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,455评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,118评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,227评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,213评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,214评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,928评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,512评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,616评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,848评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,228评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,772评论 2 339

推荐阅读更多精彩内容