文章目录

  • 前言
  • 几个主要成员类
    • 1 YYCache
    • 2 YYMemoryCache
    • 3 YYDiskCache
  • 实例化
    • 1 实例方法
    • 2 构造器方法
    • 1 检查是否有缓存
    • 2 读缓存
    • 1 写内存缓存
    • 2 写磁盘缓存
    • 1 清空内存缓存
    • 2 清空磁盘缓存
  • YYMemoryCache 初始化做了什么
  • 总结
    • 本文完

前言

YYCache是著名iOS框架YYKit的一个组件是之一, 这里有作者对这个轮子的介绍, 同时有作者对主流的几个缓存框架的性能对比. 我们以YYCache为入口, 逐个分析每个api, 学习缓存如何设计.本文的思路, 同样适用于SDWebImage的缓存策略, 只是有些细节不太一样

下面是大神对缓存策略的基本描述是这样的

缓存通常一个缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储

来一张图, 概述框架的结构

几个主要成员类

1 YYCache

用来调用YYMemoryCache和YYDiskCache的外层接口, 我们直接使用的类, 两者在使用过程中对缓存开销, 缓存时长和数量都制定了一定的策略;

2 YYMemoryCache

内存缓存负责处理容量小, 相对高速的数据;并提供了自动和手动两张删除方式且是线程安全的, 且对异步回调提供了支持

  • _YYLinkedMap
    双向链表类; YYMemoryCache使用链接映射来存取
  • _YYLinkedMapNode
    双向节点类, 供_YYLinkedMap使用

3 YYDiskCache

磁盘缓存负责处理容量大, 相对低速的. 并提供了自动和手动两张删除方式且是线程安全的, 且对异步回调提供了支持. 同时, 它可以根据所存储的内容自动的选择合适的数据类型(文件/sqlite)来获取最高的性能;

  • YYKVStorage
    YYDiskCache的底层实现类, 基于sqlite和文件系统做键值存储, 用于管理磁盘缓存
  • YYKVStorageItem
    YYKVStorage用来存储数据的单元

实例化

YYCache是线程安全的, 其中YYMemoryCache将对象负责内存缓存, YYDiskCache负责磁盘缓存; 两种存储载体有明显的区别, 内存存取速度快且容量小, 磁盘存取速度慢但容量大!

YYCache is a thread safe key-value cache.

1 实例方法

这三个api是逐级调用的, 是实例方法, 不论调用哪个, 都会创建三个实例对象, 分别是YYCache, YYMemoryCache, YYDiskCache.

- (instancetype) init;
// 名称不要重复, 否则缓存不稳定
- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;

2 构造器方法

这两个api是构造器方法创建实例; 和上面的实例方法不同的是, 这两个方法只创建了YYCache实例

+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;

1 检查是否有缓存

// 若在内存缓存中查找到对应的缓存后就会返回YES;返回结果在当前线程
- (BOOL)containsObjectForKey:(NSString *)key {return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}
// 使用block方式回调在后台队列
- (void)containsObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key, BOOL contains))block {if (!block) return;if ([_memoryCache containsObjectForKey:key]) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{block(key, YES);});} else  {[_diskCache containsObjectForKey:key withBlock:block];}
}

1.1 检查内存
读取内存缓存的策略, 我们可以看到, 在读取数据之前, 用pthread_mutex_lock获取到了互斥锁, 当前线程被锁定, 直到获取到结果. 也就是说, 此方法调用后会阻塞线程直到文件读取完成. 并且需要注意的是, 这里作者使用了LRU 淘汰算法提升查找速度

- (BOOL)containsObjectForKey:(id)key {if (!key) return NO;pthread_mutex_lock(&_lock);BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));pthread_mutex_unlock(&_lock);return contains;
}

1.2 检查磁盘
同样的, 在读取磁盘缓存时, 也使用锁操作保证安全, 即此方法也会阻塞当前线程, 直至文件读取完成.与查找内存缓存不同的是, 对磁盘缓存查找时使用了信号量来设置锁! 当信号量等于1时, 可以被当作锁来保证线程安全. 与自旋锁OSSpinLock不同的是, 使用信号量做锁不会消耗CPU资源(OSSpinLock会产生忙等), 适合用于不等待的存储过程.

// 信号量
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
self->_lock: dispatch_semaphore_t _lock;
- (BOOL)containsObjectForKey:(NSString *)key {if (!key) return NO;Lock();BOOL contains = [_kv itemExistsForKey:key];Unlock();return contains;
}
// 检查是否有指定的key
// 拿key直接去sqlite中查询
- (BOOL)itemExistsForKey:(NSString *)key {if (key.length == 0) return NO;return [self _dbGetItemCountWithKey:key] > 0;
}

2 读缓存

同样的, 可以使用key取缓存值, 原理和上述1中大致相同

// 直接返回值
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
// block返回
-(void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;

需要注意的是, 如果我们在内存缓存中没有查找到对应的值, 但是在磁盘缓存中查找到值了, 会同时把该值缓存到内存缓存中去

object = [_diskCache objectForKey:key];
f (object) {// 缓存内存[_memoryCache setObject:object forKey:key];
}

同样的, 内存缓存查找时使用了LRU 淘汰算法对双向链表进行查找(_YYLinkedMapNode是一个双向节点类)

- (id)objectForKey:(id)key {if (!key) return nil;pthread_mutex_lock(&_lock);_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));if (node) {node->_time = CACurrentMediaTime();[_lru bringNodeToHead:node];}pthread_mutex_unlock(&_lock);return node ? node->_value : nil;
}
// 把缓存节点移动到链表头部, 原位置两侧的缓存要接上
// 并且原链表头部的缓存节点要变成现在链表的第二个缓存节点
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {if (_head == node) return;if (_tail == node) {_tail = node->_prev;_tail->_next = nil;} else {node->_next->_prev = node->_prev;node->_prev->_next = node->_next;}node->_next = _head;node->_prev = nil;_head->_prev = node;_head = node;
}

在写操作的内部, 已经嵌入了更新操作

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {[_memoryCache setObject:object forKey:key];[_diskCache setObject:object forKey:key];
}
// 有回调
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key withBlock:(void (^)(void))block {[_memoryCache setObject:object forKey:key];[_diskCache setObject:object forKey:key withBlock:block];
}

我们分别对内存缓存和磁盘缓存的写操作做一个详细的分析

1 写内存缓存

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {if (!key) return;// 如果传入的值为空, 则根据key移除此缓存if (!object) {[self removeObjectForKey:key];return;}// 对当前线程添加互斥锁, 阻塞当前线程pthread_mutex_lock(&_lock);_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));NSTimeInterval now = CACurrentMediaTime();// 设置新的缓存节点至链表头部, 同时对原链表节点顺位后移if (node) {// 如果已经有值, 则更新其值, 同时保存新的时间和缓存开销_lru->_totalCost -= node->_cost;_lru->_totalCost += cost;node->_cost = cost;node->_time = now;node->_value = object;[_lru bringNodeToHead:node];} else {// 如果没有, 则新增节点同时添加对应的成员值node = [_YYLinkedMapNode new];node->_cost = cost;node->_time = now;node->_key = key;node->_value = object;// 把值和key映射保存到双向链表的某个节点中(CFDictionary是个底层容器)[_lru insertNodeAtHead:node];}// 如果开销超限, 则根据LRU算法异步的清除缓存, 直到总开销到达指定值if (_lru->_totalCost > _costLimit) {dispatch_async(_queue, ^{[self trimToCost:_costLimit];});}// 如果数量超限, 同样从尾部开始清除缓存if (_lru->_totalCount > _countLimit) {// 删除存在的尾部节点_YYLinkedMapNode *node = [_lru removeTailNode];// 如果支持异步释放if (_lru->_releaseAsynchronously) {// 判断是否支持主线程是释放// 是: 获取主队列, 否: 全局并发队列dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();dispatch_async(queue, ^{// 异步排队等待并释放节点缓存[node class]; //hold and release in queue});} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {// 回到主线程释放dispatch_async(dispatch_get_main_queue(), ^{[node class]; //hold and release in queue});}}// 解除线程阻塞, 添加内存缓存完成pthread_mutex_unlock(&_lock);
}

2 写磁盘缓存

有回调的方法和此方法类似

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {if (!key) return;if (!object) {// 和内存缓存一样 如果传入的值为空, 则根据key移除此缓存[self removeObjectForKey:key];return;}// 获取要存储对象所关联的扩展数据对象// 用户可以提前在外部调用setExtendedData类方法为要添加的值对象设置关联对象NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];NSData *value = nil;// 如果有不支持NSCoding协议的数据需要存储, 则可以在外部转为NSDataif (_customArchiveBlock) {value = _customArchiveBlock(object);} else {@try {// 归档数据, 返回值是NSDatavalue = [NSKeyedArchiver archivedDataWithRootObject:object];}@catch (NSException *exception) {// 如果不是归档要求的类型就抛出异常// 我没有找到作者为何在此处捕获异常的目的, 在网上也没有找到相关描述// 如果有大佬知晓, 烦请告知, 非常感谢// nothing to do...}}if (!value) return;NSString *filename = nil;// 根据YYKVStorage的策略, 判断是用sqlite还是文件系统存储// _kv.type是根据初始化时传入的数据对象是否超否threshold阈值来判断是哪种类型的if (_kv.type != YYKVStorageTypeSQLite) {// 判断数据长度(字节)是否超过内联阈值 默认值是20kb(20480Byte)if (value.length > _inlineThreshold) {// 则存储为文件类型filename = [self _filenameForKey:key];}}// 信号量加锁, 阻塞当前线程Lock();// 开始存储// 如果是文件类型, 则会写入文件[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];// 存储完成解锁, 释放当前线程Unlock();
}
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {if (key.length == 0 || value.length == 0) return NO;if (_type == YYKVStorageTypeFile && filename.length == 0) {return NO;}if (filename.length) {// 写入文件系统if (![self _fileWriteWithName:filename data:value]) {return NO;}// 把元数据以及关联的数据(如果有)写入dbif (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {// 如果写入失败, 则文件管理器根据文件路径删除对应的文件[self _fileDeleteWithName:filename];return NO;}return YES;} else {if (_type != YYKVStorageTypeSQLite) {// 如果非sqlite类型// 则根据key从db中取出文件名, 并删除文件NSString *filename = [self _dbGetFilenameWithKey:key];if (filename) {[self _fileDeleteWithName:filename];}}// 否则 存储为sql类型数据return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];}
}
// 私有方法, 判断是否有自定义的文件名加密方式
// 如果没有, 则默认对key MD5后形成字符串作为文件名
- (NSString *)_filenameForKey:(NSString *)key {NSString *filename = nil;if (_customFileNameBlock) filename = _customFileNameBlock(key);if (!filename) filename = _YYNSStringMD5(key);return filename;
}

清除缓存总共有五个api, 分别是

- (void)removeObjectForKey:(NSString *)key;
// 根据key清除缓存, 并异步回调
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;- (void)removeAllObjects;
// 清空所有缓存, 并异步回调
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
// 清空所有缓存, 并实时回调删除清除进度, 完成后异步回调
// 只有磁盘缓存才有进度的回调
- (void)removeAllObjectsWithProgressBlock:(void(^)(int removedCount, int totalCount))progressendBlock:(void(^)(BOOL error))end {[_memoryCache removeAllObjects];[_diskCache removeAllObjectsWithProgressBlock:progress endBlock:end];}

在这里只分析清空的情况, 其实原理都差不多

1 清空内存缓存

内存缓存的清空比较简单

- (void)removeAllObjects {// 获取互斥锁, 阻塞线程pthread_mutex_lock(&_lock);// 执行清空操作[_lru removeAll];// 释放锁, 解除阻塞线程pthread_mutex_unlock(&_lock);
}
- (void)removeAll {// 清空总内存开销和数量_totalCost = 0;_totalCount = 0;// 清除头节点和尾节点_head = nil;_tail = nil;if (CFDictionaryGetCount(_dic) > 0) {CFMutableDictionaryRef holder = _dic;_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);// 和读的时候类似, 需要判断是否支持异步释放if (_releaseAsynchronously) {// 获取队列dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();dispatch_async(queue, ^{// 在指定的队列中排队释放内存CFRelease(holder); // hold and release in specified queue});} else if (_releaseOnMainThread && !pthread_main_np()) {dispatch_async(dispatch_get_main_queue(), ^{// 主队列排队释放内存CFRelease(holder); // hold and release in specified queue});} else {// 同步的释放内存CFRelease(holder);}}
}

2 清空磁盘缓存

接下来看看磁盘缓存是如何清空的. 直接清空数据库且没有回调时,

  • 策略是先关闭db
  • 把文件全部移到垃圾桶
  • 异步串行清空垃圾桶
  • 打开数据库, 再init

原理都大同小异, 我们来讲一下带清空进度的情况.

- (void)removeAllObjectsWithProgressBlock:(void(^)(int removedCount, int totalCount))progressendBlock:(void(^)(BOOL error))end {__weak typeof(self) _self = self;dispatch_async(_queue, ^{__strong typeof(_self) self = _self;if (!self) {if (end) end(YES);return;}// 同样的, 是信号量锁住当前线程Lock();[_kv removeAllItemsWithProgressBlock:progress endBlock:end];Unlock();});
}
- (void)removeAllItemsWithProgressBlock:(void(^)(int removedCount, int totalCount))progressendBlock:(void(^)(BOOL error))end {// 获取总记录数int total = [self _dbGetTotalItemCount];if (total <= 0) {// 没有磁盘缓存, 直接回调if (end) end(total < 0);} else {int left = total;int perCount = 32;NSArray *items = nil;BOOL suc = NO;do {// 每次查找到d前32位数据items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];// 遍历数据, 挨个清除for (YYKVStorageItem *item in items) {if (left > 0) {// 清除文件类型数据if (item.filename) {[self _fileDeleteWithName:item.filename];}// 清除sql类型数据suc = [self _dbDeleteItemWithKey:item.key];left--;} else {// 直到清除完毕break;}if (!suc) break;}// 每删除完32条数据, 回调一次进度if (progress) progress(total - left, total);} while (left > 0 && items.count > 0 && suc);// 检查db状态if (suc) [self _dbCheckpoint];if (end) end(!suc);}
}

YYMemoryCache 初始化做了什么

现在我们来分析一下, YYMemoryCache的初始化过程, 来更进一步的了解其原理(YYDiskCache有些区别, 但是大致思想是相同的)

- (instancetype)init {self = super.init;// 创建了一个互斥锁, 并赋值给锁成员pthread_mutex_init(&_lock, NULL);// 初始化双向链表, 并赋值给成员_lru = [_YYLinkedMap new];// 初始化串行队列, 并赋值给成员_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);// 缓存数量_countLimit = NSUIntegerMax;// 缓存开销_costLimit = NSUIntegerMax;// 缓存过期时间_ageLimit = DBL_MAX;_autoTrimInterval = 5.0;// 这两个是什么意思呢// 这里用到了观察者模式, 把当前对象设置为观察者并注册到通知中心// 如果观察者(self)收到内存警告是否可以清除缓存, 默认YES_shouldRemoveAllObjectsOnMemoryWarning = YES;// 收到内存警告后的清除策略, 默认在后台清除_shouldRemoveAllObjectsWhenEnteringBackground = YES;[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];// 后面内部会设置一些其他的配置// 如设置缓存限制, 数量限制, 时间限制等// 如autoTrimInterval设置自动检测内存的时间, 并由yycache维护的定时器进行管理(但是我并没有找到这个定时器, 哪位大佬告知一下)// 这些操作都是异步串行队列中延时执行[self _trimRecursively];return self;
}

总结

我们上面简单讲述了YYCache组件的各个api的调用流程; 我们来简单总结一下, 这个轮子带给我们学习的地方(学习的地方太多了, 重点讲几个), 以及需要注意的地方
1 大家知道如果想高效的查询数据, 使用字典是一个很好的方法; 在数据结构中, 我们可以把字典理解为一个哈希表, 通过key和内存地址进行映射, 同时这中数据结构可以为后面的哈希查找等算法提供高效的支持.
2 不论是内存缓存还是磁盘缓存, 思想都是值得我们借鉴的. 比如 把key进行md5转换作为文件名映射到内存中等. 比如查找策略, 利用LRU算法提高命中率. 在SDWebImage中的实现思想也有相似的地方, 我们自己可以做一些对比;
3 说一个我发现的问题,但是我并没有验证, 只做一个猜测! 不论是YYWebImage还是SDWebImage的缓存策略, 当没有内存超限的时候都不会对内存进行释放, 如果我们对内存进行优化的过程中, 可以考虑主动的清除一些缓存, 以防在内存敏感的页面引发问题.
4 对于想了解其中算法(LRU和MRU)的同学请移步大佬写的常用淘汰算法;

本文完

我是个Coder界的小学生, 如有不足, 万望不吝指教

转载请注明作者和链接哦!
参考资料:
YYCache 设计思路

深入理解YYCache缓存策略相关推荐

  1. Java基础————理解Integer对象的缓存策略

    一个简单的面试题 public static void main(String[] args) {Integer in1 = 100;Integer in2 = 100;Integer in3 = 2 ...

  2. 电子商务网站比较常用的缓存策略架构

    缓存是分布式系统中的重要组件,主要解决高并发,大数据场景下,热点数据访问的性能问题.提供高性能的数据快速访问. 这次主要是分享下自己觉得比较通用的一个缓存策略的架构方案,也是比较 容易理解的.欢迎吐槽 ...

  3. iOS SDWebImage 缓存机制与缓存策略

    2019独角兽企业重金招聘Python工程师标准>>> 一.SDWebImage 缓存机制 1.基本用法 SDWebImage提供一个UIImageView的Category,用来加 ...

  4. 彻底弄懂 HTTP 缓存机制 —— 基于缓存策略三要素分解法

    导语 HTTP 缓存机制作为 Web 性能优化的重要手段,对从事 Web 开发的小伙伴们来说是必须要掌握的知识,但最近我遇到了几个缓存头设置相关的题目,发现有好几道题答错了,有的甚至在知道了正确答案后 ...

  5. extjs中js资源缓存策略

    http的缓存协商 浏览器对静态文件的缓存主要是通过cache-control来控制的,cache-control可以设置no-cache,max-age以及must-revalidate等来设置缓存 ...

  6. 【腾讯Bugly干货分享】彻底弄懂 Http 缓存机制 - 基于缓存策略三要素分解法

    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/qOMO0LIdA47j3RjhbCWUEQ 作者:李 ...

  7. 看剧流畅还省电?视频类应用预缓存策略功耗评测详解

    你是否遇到过这样的问题,在疯狂追剧时手机电量消耗过快,一度以为是屏幕亮度等引起?但当在相同的屏幕亮度.音量.网络环境(WiFi网络)等条件下刷同一部剧,不同视频类应用的耗电量仍不同. 那么还有哪些因素 ...

  8. Glide 4.9源码解析-缓存策略

    本文Glide源码基于4.9,版本下载地址如下:Glide 4.9 前言 在分析了Glide的图片加载流程后,更加发觉到Glide的强大,于是这篇文章将继续深入分析Glide的缓存策略.不过今天的文章 ...

  9. 理解Memcached缓存[转载]

    本文讨论了使用Memcached时,到底要缓存什么的问题,值得深入讨论,与大家共享. 最近公司一直在招人,我作为主考官之一 .经常会提问的一个问题,就是让用户介绍自己在缓存方面的经验和心得.绝大多数的 ...

最新文章

  1. AMiner新功能:技术趋势分析—挖掘技术源头、近期热度和全局热度
  2. 在IIS上部署基于django WEB框架的python网站应用
  3. JAVA--虚函数,抽象函数,抽象类,接口
  4. Codeforces 360E 贪心 最短路
  5. c 如何操作php,thinkphp的c方法使用示例
  6. NHibernate学习--初识NHibernate
  7. 大厂产品经理是如何做好用数据驱动业务增长的?
  8. linux 内存管理 ppt,Linux内存管理 Memory Manager.ppt
  9. PAT 乙级 1041. 考试座位号(15) Java版
  10. 类与对象- 课后作业1
  11. modbus学习笔记——帧
  12. quartus支持linux系统,在64位Linux下把Quartus II设置成64位的方法
  13. Unity Demo ——3D时钟
  14. CYQ.Data V5 分布式自动化缓存设计介绍
  15. 【实验】SVO2.0 待更新
  16. 【GitHub】中SSH key的配置
  17. 操作系统--磁盘调度题目
  18. Do we need an operating system?
  19. 100句温柔又体贴的话
  20. 校企合作,人才共育|岳阳开放大学校长乐艳华一行莅临云畅科技考察交流

热门文章

  1. 模型压缩 方法汇总和梳理
  2. 责怪用户、NSA、盗版、朝鲜?Wannacry勒索病毒这锅还得微软背
  3. aiohttp的使用
  4. 鸿蒙升级设备计划,鸿蒙品牌升级,3亿设备将搭载“超级终端”
  5. 利用adams建立含间隙的关节
  6. N63044-第十三周
  7. leetcode探索专题中的初级算法练习题(python代码+解题思路)
  8. php 判断 平板,PHP代码判断设备是手机还是平板电脑(两种方法)_php实例
  9. ARMv7与ARMv8的区别
  10. iOS UIButton之UIControlEvents介绍