YYCache是国内开发者ibireme开源的一个线程安全的高性能缓存组件,代码风格简洁清晰,在GitHub上已经有了1600+颗星。前端
阅读它的源码有助于创建比较完整的缓存设计的思路,同时也能巩固一下双向链表,线程锁,数据库操做相关的知识。若是你尚未看过YYCache的源码,那么恭喜你,阅读此文会对理解YYCache的源码有比较大的帮助。node
在正式开始讲解源码以前,先简单看一下该框架的使用方法。git
举一个缓存用户姓名的例子来看一下YYCache的几个API:程序员
//须要缓存的对象
NSString *userName = @"Jack";
//须要缓存的对象在缓存里对应的键
NSString *key = @"user_name";
//建立一个YYCache实例:userInfoCache
YYCache *userInfoCache = [YYCache cacheWithName:@"userInfo"];
//存入键值对
[userInfoCache setObject:userName forKey:key withBlock:^{
NSLog(@"caching object succeed");
}];
//判断缓存是否存在
[userInfoCache containsObjectForKey:key withBlock:^(NSString * _Nonnull key, BOOL contains) {
if (contains){
NSLog(@"object exists");
}
}];
//根据key读取数据
[userInfoCache objectForKey:key withBlock:^(NSString * _Nonnull key, id<NSCoding> _Nonnull object) {
NSLog(@"user name : %@",object);
}];
//根据key移除缓存
[userInfoCache removeObjectForKey:key withBlock:^(NSString * _Nonnull key) {
NSLog(@"remove user name %@",key);
}];
//移除全部缓存
[userInfoCache removeAllObjectsWithBlock:^{
NSLog(@"removing all cache succeed");
}];
//移除全部缓存带进度
[userInfoCache removeAllObjectsWithProgressBlock:^(int removedCount, int totalCount) {
NSLog(@"remove all cache objects: removedCount :%d totalCount : %d",removedCount,totalCount);
} endBlock:^(BOOL error) {
if(!error){
NSLog(@"remove all cache objects: succeed");
}else{
NSLog(@"remove all cache objects: failed");
}
}];
复制代码
整体来看这些API与NSCache
是差很少的。 再来看一下框架的架构图与成员职责划分。github
从架构图上来看,该组件里面的成员并很少:算法
知道了YYCache的架构图与成员职责划分之后,如今结合代码开始正式讲解。 讲解分为下面6个部分:sql
YYCache给用户提供全部最外层的缓存操做接口,而这些接口的内部内部其实是调用了YYMemoryCache和YYDiskCache对象的相关方法。数据库
咱们来看一下YYCache的属性和接口:编程
@interface YYCache : NSObject
@property (copy, readonly) NSString *name;//缓存名称
@property (strong, readonly) YYMemoryCache *memoryCache;//内存缓存
@property (strong, readonly) YYDiskCache *diskCache;//磁盘缓存
//是否包含某缓存,无回调
- (BOOL)containsObjectForKey:(NSString *)key;
//是否包含某缓存,有回调
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;
//获取缓存对象,无回调
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
//获取缓存对象,有回调
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;
//写入缓存对象,无回调
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
//写入缓存对象,有回调
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;
//移除某缓存,无回调
- (void)removeObjectForKey:(NSString *)key;
//移除某缓存,有回调
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;
//移除全部缓存,无回调
- (void)removeAllObjects;
//移除全部缓存,有回调
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
//移除全部缓存,有进度和完成的回调
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
@end
复制代码
从上面的接口能够看出YYCache的接口和NSCache很相近,并且在接口上都区分了有无回调的功能。 下面结合代码看一下这些接口是如何实现的:后端
下面省略了带有回调的接口,由于与无回调的接口很是接近。
- (BOOL)containsObjectForKey:(NSString *)key {
//先检查内存缓存是否存在,再检查磁盘缓存是否存在
return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}
- (id<NSCoding>)objectForKey:(NSString *)key {
//首先尝试获取内存缓存,而后获取磁盘缓存
id<NSCoding> object = [_memoryCache objectForKey:key];
//若是内存缓存不存在,就会去磁盘缓存里面找:若是找到了,则再次写入内存缓存中;若是没找到,就返回nil
if (!object) {
object = [_diskCache objectForKey:key];
if (object) {
[_memoryCache setObject:object forKey:key];
}
}
return object;
}
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
//先写入内存缓存,后写入磁盘缓存
[_memoryCache setObject:object forKey:key];
[_diskCache setObject:object forKey:key];
}
- (void)removeObjectForKey:(NSString *)key {
//先移除内存缓存,后移除磁盘缓存
[_memoryCache removeObjectForKey:key];
[_diskCache removeObjectForKey:key];
}
- (void)removeAllObjects {
//先所有移除内存缓存,后所有移除磁盘缓存
[_memoryCache removeAllObjects];
[_diskCache removeAllObjects];
}
复制代码
从上面的接口实现能够看出:在YYCache中,永远都是先访问内存缓存,而后再访问磁盘缓存(包括了写入,读取,查询,删除缓存的操做)。并且关于内存缓存(_memoryCache)的操做,是不存在block回调的。
值得一提的是:在读取缓存的操做中,若是在内存缓存中没法获取对应的缓存,则会去磁盘缓存中寻找。若是在磁盘缓存中找到了对应的缓存,则会将该对象再次写入内存缓存中,保证在下一次尝试获取同一缓存时可以在内存中就能返回,提升速度。
OK,如今了解了YYCache的接口以及实现,下面我分别讲解一下YYMemoryCache(内存缓存)和YYDiskCache(磁盘缓存)这两个类。
YYMemoryCache负责处理容量小,相对高速的内存缓存:它将须要缓存的对象与传入的key关联起来,操做相似于NSCache。
可是与NSCache不一样的是,YYMemoryCache的内部有:
一个是淘汰算法,另外一个是清理维度,乍一看可能没什么太大区别。我在这里先简单区分一下:
缓存淘汰算法的目的在于区分出使用频率高和使用频率低的缓存,当缓存数量达到必定限制的时候会优先清理那些使用频率低的缓存。由于使用频率已经比较低的缓存在未来的使用频率也颇有可能会低。
缓存清理维度是给每一个缓存添加的标记:
若是用户须要删除age(距上一次的访问时间)超过1天的缓存,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始查找,直到全部距上一次的访问时间超过1天的缓存都清理掉为止。
若是用户须要将缓存总开销清理到总开销小于或等于某个值,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始清理,直到总开销小于或等于这个值。
若是用户须要将缓存总数清理到总开销小于或等于某个值,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始清理,直到总开销小于或等于这个值。
能够看出,不管是以哪一个维度来清理缓存,都是从缓存使用频率最低的那个缓存开始清理。而YYMemoryCache保留的全部缓存的使用频率的高低,是由LRU这个算法决定的。
如今知道了这两者的区别,下面来具体讲解一下缓存淘汰算法和缓存清理策略:
在详细讲解这个算法以前我以为有必要先说一下该算法的核心:
我我的认为LRU缓存替换策略的核心在于若是某个缓存访问的频率越高,就认定用户在未来越有可能访问这个缓存。 因此在这个算法中,将那些最新访问(写入),最屡次被访问的缓存移到最前面,而后那些很早以前写入,不常常访问的缓存就被自动放在了后面。这样一来,在保留的缓存个数必定的状况下,留下的缓存都是访问频率比较高的,这样一来也就提高了缓存的命中率。谁都不想留着一些很难被用户再次访问的缓存,毕竟缓存自己也占有必定的资源不是么?
其实这个道理和一些商城类app的商品推荐逻辑是同样的: 若是首页只能展现10个商品,对于一个程序员用户来讲,可能推荐的是于那些他最近购买商品相似的机械键盘鼠标,技术书籍或者显示屏之类的商品,而不是一些洋娃娃或是钢笔之类的商品。
那么LRU算法具体是怎么作的呢?
在YYMemoryCache中,使用了双向链表这个数据结构来保存这些缓存:
这样一来,就能够保证链表前端的缓存是最近写入过和常常访问过的。并且该算法老是从链表的最后端删除缓存,这也就保证了留下的都是一些“比较新鲜的”缓存。
下面结合代码来说解一下这个算法的实现:
YYMemoryCache用一个链表节点类来保存某个单独的内存缓存的信息(键,值,缓存时间等),而后用一个双向链表类来保存和管理这些节点。这两个类的名称分别是:
_YYLinkedMapNode能够被看作是对某个缓存的封装:它包含了该节点上一个和下一个节点的指针,以及缓存的key和对应的值(对象),还有该缓存的开销和访问时间。
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key; //缓存key
id _value; //key对应值
NSUInteger _cost; //缓存开销
NSTimeInterval _time; //访问时间
}
@end
@implementation _YYLinkedMapNode
@end
复制代码
下面看一下双向链表类:
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // 用于存放节点
NSUInteger _totalCost; //总开销
NSUInteger _totalCount; //节点总数
_YYLinkedMapNode *_head; // 链表的头部结点
_YYLinkedMapNode *_tail; // 链表的尾部节点
BOOL _releaseOnMainThread; //是否在主线程释放,默认为NO
BOOL _releaseAsynchronously; //是否在子线程释放,默认为YES
}
//在链表头部插入某节点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
//将链表内部的某个节点移到链表头部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
//移除某个节点
- (void)removeNode:(_YYLinkedMapNode *)node;
//移除链表的尾部节点并返回它
- (_YYLinkedMapNode *)removeTailNode;
//移除全部节点(默认在子线程操做)
- (void)removeAll;
@end
复制代码
从链表类的属性上看:链表类内置了CFMutableDictionaryRef,用于保存节点的键值对,它还持有了链表内节点的总开销,总数量,头尾节点等数据。
能够参考下面这张图来看一下两者的关系:
看一下_YYLinkedMap的接口的实现:
将节点插入到链表头部:
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
//设置该node的值
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
//增长开销和总缓存数量
_totalCost += node->_cost;
_totalCount++;
if (_head) {
//若是链表内已经存在头节点,则将这个头节点赋给当前节点的尾指针(原第一个节点变成了现第二个节点)
node->_next = _head;
//将该节点赋给现第二个节点的头指针(此时_head指向的节点是先第二个节点)
_head->_prev = node;
//将该节点赋给链表的头结点指针(该节点变成了现第一个节点)
_head = node;
} else {
//若是链表内没有头结点,说明是空链表。说明是第一次插入,则将链表的头尾节点都设置为当前节点
_head = _tail = node;
}
}
复制代码
要看懂节点操做的代码只要了解双向链表的特性便可。在双向链表中:
为了便于理解,咱们能够把这个抽象概念类比于幼儿园手拉手的小朋友们: 每一个小朋友的左手都拉着前面小朋友的右手;每一个小朋友的右手都拉着后面小朋友的左手; 并且最前面的小朋友的左手和最后面的小朋友的右手都没有拉任何一个小朋友。
将某个节点移动到链表头部:
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
//若是该节点已是链表头部节点,则当即返回,不作任何操做
if (_head == node) return;
if (_tail == node) {
//若是该节点是链表的尾部节点
//1. 将该节点的头指针指向的节点变成链表的尾节点(将倒数第二个节点变成倒数第一个节点,即尾部节点)
_tail = node->_prev;
//2. 将新的尾部节点的尾部指针置空
_tail->_next = nil;
} else {
//若是该节点是链表头部和尾部之外的节点(中间节点)
//1. 将该node的头指针指向的节点赋给其尾指针指向的节点的头指针
node->_next->_prev = node->_prev;
//2. 将该node的尾指针指向的节点赋给其头指针指向的节点的尾指针
node->_prev->_next = node->_next;
}
//将原头节点赋给该节点的尾指针(原第一个节点变成了现第二个节点)
node->_next = _head;
//将当前节点的头节点置空
node->_prev = nil;
//将现第二个节点的头结点指向当前节点(此时_head指向的节点是现第二个节点)
_head->_prev = node;
//将该节点设置为链表的头节点
_head = node;
}
复制代码
第一次看上面的代码我本身是懵逼的,不过若是结合上面小朋友拉手的例子就能够快一点理解。 若是要其中一个小朋友放在队伍的最前面,须要
上面说的比较简略,可是相信对你们理解整个过程会有帮助。
也能够再结合链表的图解来看一下:
读者一样能够利用这种思考方式理解下面这段代码:
移除链表中的某个节点:
- (void)removeNode:(_YYLinkedMapNode *)node {
//除去该node的键对应的值
CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
//减去开销和总缓存数量
_totalCost -= node->_cost;
_totalCount--;
//节点操做
//1. 将该node的头指针指向的节点赋给其尾指针指向的节点的头指针
if (node->_next) node->_next->_prev = node->_prev;
//2. 将该node的尾指针指向的节点赋给其头指针指向的节点的尾指针
if (node->_prev) node->_prev->_next = node->_next;
//3. 若是该node就是链表的头结点,则将该node的尾部指针指向的节点赋给链表的头节点(第二变成了第一)
if (_head == node) _head = node->_next;
//4. 若是该node就是链表的尾节点,则将该node的头部指针指向的节点赋给链表的尾节点(倒数第二变成了倒数第一)
if (_tail == node) _tail = node->_prev;
}
复制代码
移除并返回尾部的node:
- (_YYLinkedMapNode *)removeTailNode {
//若是不存在尾节点,则返回nil
if (!_tail) return nil;
_YYLinkedMapNode *tail = _tail;
//移除尾部节点对应的值
CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key));
//减小开销和总缓存数量
_totalCost -= _tail->_cost;
_totalCount--;
if (_head == _tail) {
//若是链表的头尾节点相同,说明链表只有一个节点。将其置空
_head = _tail = nil;
} else {
//将链表的尾节指针指向的指针赋给链表的尾指针(倒数第二变成了倒数第一)
_tail = _tail->_prev;
//将新的尾节点的尾指针置空
_tail->_next = nil;
}
return tail;
}
复制代码
OK,如今了解了YYMemoryCache底层的节点操做的代码。如今来看一下YYMemoryCache是如何使用它们的。
//YYMemoryCache.h
@interface YYMemoryCache : NSObject
#pragma mark - Attribute
//缓存名称,默认为nil
@property (nullable, copy) NSString *name;
//缓存总数量
@property (readonly) NSUInteger totalCount;
//缓存总开销
@property (readonly) NSUInteger totalCost;
#pragma mark - Limit
//数量上限,默认为NSUIntegerMax,也就是无上限
@property NSUInteger countLimit;
//开销上限,默认为NSUIntegerMax,也就是无上限
@property NSUInteger costLimit;
//缓存时间上限,默认为DBL_MAX,也就是无上限
@property NSTimeInterval ageLimit;
//清理超出上限以外的缓存的操做间隔时间,默认为5s
@property NSTimeInterval autoTrimInterval;
//收到内存警告时是否清理全部缓存,默认为YES
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;
//app进入后台是是否清理全部缓存,默认为YES
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
//收到内存警告的回调block
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
//进入后台的回调block
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
//缓存清理是否在后台进行,默认为NO
@property BOOL releaseOnMainThread;
//缓存清理是否异步执行,默认为YES
@property BOOL releaseAsynchronously;
#pragma mark - Access Methods
//是否包含某个缓存
- (BOOL)containsObjectForKey:(id)key;
//获取缓存对象
- (nullable id)objectForKey:(id)key;
//写入缓存对象
- (void)setObject:(nullable id)object forKey:(id)key;
//写入缓存对象,并添加对应的开销
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
//移除某缓存
- (void)removeObjectForKey:(id)key;
//移除全部缓存
- (void)removeAllObjects;
#pragma mark - Trim
// =========== 缓存清理接口 ===========
//清理缓存到指定个数
- (void)trimToCount:(NSUInteger)count;
//清理缓存到指定开销
- (void)trimToCost:(NSUInteger)cost;
//清理缓存时间小于指定时间的缓存
- (void)trimToAge:(NSTimeInterval)age;
复制代码
在YYMemoryCache的初始化方法里,会实例化一个_YYLinkedMap的实例来赋给_lru这个成员变量。
- (instancetype)init{
....
_lru = [_YYLinkedMap new];
...
}
复制代码
而后全部的关于缓存的操做,都要用到_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;
}
//获取某个缓存对象
- (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;
}
//写入某个缓存对象,开销默认为0
- (void)setObject:(id)object forKey:(id)key {
[self setObject:object forKey:key withCost:0];
}
//写入某个缓存对象,并存入缓存开销
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
NSTimeInterval now = CACurrentMediaTime();
if (node) {
//若是存在与传入的key值匹配的node,则更新该node的value,cost,time,并将这个node移到链表头部
//更新总cost
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
//更新node
node->_cost = cost;
node->_time = now;
node->_value = object;
//将node移动至链表头部
[_lru bringNodeToHead:node];
} else {
//若是不存在与传入的key值匹配的node,则新建一个node,将key,value,cost,time赋给它,并将这个node插入到链表头部
//新建node,并赋值
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
//将node插入至链表头部
[_lru insertNodeAtHead:node];
}
//若是cost超过了限制,则进行删除缓存操做(从链表尾部开始删除,直到符合限制要求)
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
//若是total count超过了限制,则进行删除缓存操做(从链表尾部开始删除,删除一次便可)
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);
}
//移除某个缓存对象
- (void)removeObjectForKey:(id)key {
if (!key) return;
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
//内部调用了链表的removeNode:方法
[_lru removeNode:node];
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);
}
//内部调用了链表的removeAll方法
- (void)removeAllObjects {
pthread_mutex_lock(&_lock);
[_lru removeAll];
pthread_mutex_unlock(&_lock);
}
复制代码
上面的实现是针对缓存的查询,写入,获取操做的,接下来看一下缓存的清理策略。
如上文所说,在YYCache中,缓存的清理能够从缓存总数量,缓存总开销,缓存距上一次的访问时间来清理缓存。并且每种维度的清理操做均可以分为自动和手动的方式来进行。
缓存的自动清理功能在YYMemoryCache初始化以后就开始了,是一个递归调用的实现:
//YYMemoryCache.m
- (instancetype)init{
...
//开始按期清理
[self _trimRecursively];
...
}
//递归清理,相隔时间为_autoTrimInterval,在初始化以后当即执行
- (void)_trimRecursively {
__weak typeof(self) _self = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)),
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong typeof(_self) self = _self;
if (!self) return;
//在后台进行清理操做
[self _trimInBackground];
//调用本身,递归操做
[self _trimRecursively];
});
}
//清理全部不符合限制的缓存,顺序为:cost,count,age
- (void)_trimInBackground {
dispatch_async(_queue, ^{
[self _trimToCost:self->_costLimit];
[self _trimToCount:self->_countLimit];
[self _trimToAge:self->_ageLimit];
});
}
复制代码
//YYMemoryCache.m
- (void)trimToCount:(NSUInteger)count {
if (count == 0) {
[self removeAllObjects];
return;
}
[self _trimToCount:count];
}
- (void)trimToCost:(NSUInteger)cost {
[self _trimToCost:cost];
}
- (void)trimToAge:(NSTimeInterval)age {
[self _trimToAge:age];
}
复制代码
能够看到,YYMemoryCache是按照缓存数量,缓存开销,缓存时间的顺序来自动清空缓存的。咱们结合代码看一下它是如何按照缓存数量来清理缓存的(其余两种清理方式相似,暂不给出):
//YYMemoryCache.m
//将内存缓存数量降至等于或小于传入的数量;若是传入的值为0,则删除所有内存缓存
- (void)_trimToCount:(NSUInteger)countLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
//若是传入的参数=0,则删除全部内存缓存
if (countLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCount <= countLimit) {
//若是当前缓存的总数量已经小于或等于传入的数量,则直接返回YES,不进行清理
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;
NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
//==0的时候说明在尝试加锁的时候,获取锁成功,从而能够进行操做;不然等待10秒(可是不知道为何是10s而不是2s,5s,等等)
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_totalCount > countLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
usleep(10 * 1000); //10 ms
}
}
if (holder.count) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // release in queue
});
}
}
复制代码
其实上面这三种清理的方法在YYMemoryCache封装成了接口,因此用户也能够经过YYCache的memoryCache这个属性来手动清理相应维度上不符合传入标准的缓存:
//YYMemoryCache.h
// =========== 缓存清理接口 ===========
//清理缓存到指定个数
- (void)trimToCount:(NSUInteger)count;
//清理缓存到指定开销
- (void)trimToCost:(NSUInteger)cost;
//清理缓存时间小于指定时间的缓存
- (void)trimToAge:(NSTimeInterval)age;
复制代码
看一下它们的实现:
//清理缓存到指定个数
- (void)trimToCount:(NSUInteger)count {
if (count == 0) {
[self removeAllObjects];
return;
}
[self _trimToCount:count];
}
//清理缓存到指定开销
- (void)trimToCost:(NSUInteger)cost {
[self _trimToCost:cost];
}
//清理缓存时间小于指定时间的缓存
- (void)trimToAge:(NSTimeInterval)age {
[self _trimToAge:age];
}
复制代码
YYDiskCache负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操做。做为YYCache的第二级缓存,它与第一级缓存YYMemoryCache的相同点是:
它与YYMemoryCache不一样点是:
这里须要说明的是: 对于上面的第一条:我看源码的时候只看出来有这两种缓存形式,可是从内部的缓存type枚举来看,实际上是分为三种的:
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0,
YYKVStorageTypeSQLite = 1,
YYKVStorageTypeMixed = 2,
};
复制代码
也就是说我只找到了第二,第三种缓存形式,而第一种纯粹的文件存储(YYKVStorageTypeFile)形式的实现我没有找到:当type为 YYKVStorageTypeFile和YYKVStorageTypeMixed的时候的缓存实现都是一致的:都是讲data存在文件里,将元数据放在数据库里面。
在YYDiskCache的初始化方法里,没有发现正确的将缓存类型设置为YYKVStorageTypeFile的方法:
//YYDiskCache.m
- (instancetype)init {
@throw [NSException exceptionWithName:@"YYDiskCache init error" reason:@"YYDiskCache must be initialized with a path. Use 'initWithPath:' or 'initWithPath:inlineThreshold:' instead." userInfo:nil];
return [self initWithPath:@"" inlineThreshold:0];
}
- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}
...
}
复制代码
从上面的代码能够看出来,当给指定初始化方法initWithPath:inlineThreshold:
的第二个参数传入0的时候,缓存类型才是YYKVStorageTypeFile。并且比较经常使用的初始化方法initWithPath:
的实现里,是将20kb传入了指定初始化方法里,结果就是将type设置成了YYKVStorageTypeMixed。
并且我也想不出若是只有文件形式的缓存的话,其元数据如何保存。若是有读者知道的话,麻烦告知一下,很是感谢了~~
在本文暂时对于上面提到的”文件+数据库的形式”在下文统一说成文件缓存了。
在接口的设计上,YYDiskCache与YYMemoryCache是高度一致的,只不过由于有些时候大文件的访问可能会比较耗时,因此框架做者在保留了与YYMemoryCache同样的接口的基础上,还在原来的基础上添加了block回调,避免阻塞线程。来看一下YYDiskCache的接口(省略了注释):
//YYDiskCache.h
- (BOOL)containsObjectForKey:(NSString *)key;
- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block;
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id<NSCoding> _Nullable object))block;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(void(^)(void))block;
- (void)removeObjectForKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key))block;
- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
- (NSInteger)totalCount;
- (void)totalCountWithBlock:(void(^)(NSInteger totalCount))block;
- (NSInteger)totalCost;
- (void)totalCostWithBlock:(void(^)(NSInteger totalCost))block;
#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCount:(NSUInteger)count withBlock:(void(^)(void))block;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToCost:(NSUInteger)cost withBlock:(void(^)(void))block;
- (void)trimToAge:(NSTimeInterval)age;
- (void)trimToAge:(NSTimeInterval)age withBlock:(void(^)(void))block;
复制代码
从上面的接口代码能够看出,YYDiskCache与YYMemoryCache在接口设计上是很是类似的。可是,YYDiskCache有一个很是重要的属性,它做为用sqlite作缓存仍是用文件作缓存的分水岭:
//YYDiskCache.h
@property (readonly) NSUInteger inlineThreshold;
复制代码
这个属性的默认值是20480byte,也就是20kb。便是说,若是缓存数据的长度大于这个值,就使用文件存储;若是小于这个值,就是用sqlite存储。来看一下这个属性是如何使用的:
首先咱们会在YYDiskCache的指定初始化方法里看到这个属性:
//YYDiskCache.m
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
_inlineThreshold = threshold;
...
}
复制代码
在这里将_inlineThreshold赋值,也是惟一一次的赋值。而后在写入缓存的操做里判断写入缓存的大小是否大于这个临界值,若是是,则使用文件缓存:
//YYDiskCache.m
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
...
NSString *filename = nil;
if (_kv.type != YYKVStorageTypeSQLite) {
//若是长度大临界值,则生成文件名称,使得filename不为nil
if (value.length > _inlineThreshold) {
filename = [self _filenameForKey:key];
}
}
Lock();
//在该方法内部判断filename是否为nil,若是是,则使用sqlite进行缓存;若是不是,则使用文件缓存
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
复制代码
如今咱们知道了YYDiskCache相对于YYMemoryCache最大的不一样之处是缓存类型的不一样。 细心的朋友会发现上面这个写入缓存的方法(saveItemWithKey:value:filename:extendedData:)其实是属于_kv的。这个_kv就是上面提到的YYKVStorage的实例,它在YYDiskCache的初始化方法里被赋值:
//YYDiskCache.m
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
if (!kv) return nil;
_kv = kv;
...
}
复制代码
一样地,再举其余两个接口为例,内部也是调用了_kv的方法:
- (BOOL)containsObjectForKey:(NSString *)key {
if (!key) return NO;
Lock();
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
return contains;
}
- (void)removeObjectForKey:(NSString *)key {
if (!key) return;
Lock();
[_kv removeItemForKey:key];
Unlock();
}
复制代码
因此是时候来看一下YYKVStorage的接口和实现了:
YYKVStorage实例负责保存和管理全部磁盘缓存。和YYMemoryCache里面的_YYLinkedMap将缓存封装成节点类_YYLinkedMapNode相似,YYKVStorage也将某个单独的磁盘缓存封装成了一个类,这个类就是YYKVStorageItem,它保存了某个缓存所对应的一些信息(key, value, 文件名,大小等等):
//YYKVStorageItem.h
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; //键
@property (nonatomic, strong) NSData *value; //值
@property (nullable, nonatomic, strong) NSString *filename; //文件名
@property (nonatomic) int size; //值的大小,单位是byte
@property (nonatomic) int modTime; //修改时间戳
@property (nonatomic) int accessTime; //最后访问的时间戳
@property (nullable, nonatomic, strong) NSData *extendedData; //extended data
@end
复制代码
既然在这里将缓存封装成了YYKVStorageItem实例,那么做为缓存的管理者,YYKVStorage就必然有操做YYKVStorageItem的接口了:
//YYKVStorage.h
//写入某个item
- (BOOL)saveItem:(YYKVStorageItem *)item;
//写入某个键值对,值为NSData对象
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
//写入某个键值对,包括文件名以及data信息
- (BOOL)saveItemWithKey:(NSString *)key
value:(NSData *)value
filename:(nullable NSString *)filename
extendedData:(nullable NSData *)extendedData;
#pragma mark - Remove Items
//移除某个键的item
- (BOOL)removeItemForKey:(NSString *)key;
//移除多个键的item
- (BOOL)removeItemForKeys:(NSArray<NSString *> *)keys;
//移除大于参数size的item
- (BOOL)removeItemsLargerThanSize:(int)size;
//移除时间早于参数时间的item
- (BOOL)removeItemsEarlierThanTime:(int)time;
//移除item,使得缓存总容量小于参数size
- (BOOL)removeItemsToFitSize:(int)maxSize;
//移除item,使得缓存数量小于参数size
- (BOOL)removeItemsToFitCount:(int)maxCount;
//移除全部的item
- (BOOL)removeAllItems;
//移除全部的item,附带进度与结束block
- (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
#pragma mark - Get Items
//读取参数key对应的item
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
//读取参数key对应的data
- (nullable NSData *)getItemValueForKey:(NSString *)key;
//读取参数数组对应的item数组
- (nullable NSArray<YYKVStorageItem *> *)getItemForKeys:(NSArray<NSString *> *)keys;
//读取参数数组对应的item字典
- (nullable NSDictionary<NSString *, NSData *> *)getItemValueForKeys:(NSArray<NSString *> *)keys;
复制代码
你们最关心的应该是写入缓存的接口是如何实现的,下面重点讲一下写入缓存的接口:
//写入某个item
- (BOOL)saveItem:(YYKVStorageItem *)item;
//写入某个键值对,值为NSData对象
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
//写入某个键值对,包括文件名以及data信息
- (BOOL)saveItemWithKey:(NSString *)key
value:(NSData *)value
filename:(nullable NSString *)filename
extendedData:(nullable NSData *)extendedData;
复制代码
这三个接口都比较相似,上面的两个方法都会调用最下面参数最多的方法。在详细讲解写入缓存的代码以前,我先讲一下写入缓存的大体逻辑,有助于让你们理解整个YYDiskCache写入缓存的流程:
- (BOOL)saveItem:(YYKVStorageItem *)item {
return [self saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData];
}
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value {
return [self saveItemWithKey:key value:value filename:nil extendedData:nil];
}
- (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;
}
//写入元数据
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
//若是缓存信息保存失败,则删除对应的文件
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
//若是文件名为空字符串,说明不要进行文件缓存
if (_type != YYKVStorageTypeSQLite) {
//若是缓存类型不是数据库缓存,则查找出相应的文件名并删除
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
// 缓存类型是数据库缓存,把元数据和value写入数据库
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
复制代码
从上面的代码能够看出,在底层写入缓存的方法是_dbSaveWithKey:value:fileName:extendedData:
,这个方法使用了两次:
不过虽然调用了两次,咱们能够从传入的参数是有差异的:第二次filename传了nil。那么咱们来看一下_dbSaveWithKey:value:fileName:extendedData:
内部是如何区分有无filename的状况的:
下面结合代码看一下:
//数据库存储
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
//sql语句
NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;
int timestamp = (int)time(NULL);
//key
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
//filename
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
//size
sqlite3_bind_int(stmt, 3, (int)value.length);
//inline_data
if (fileName.length == 0) {
//若是文件名长度==0,则将value存入数据库
sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
} else {
//若是文件名长度不为0,则不将value存入数据库
sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}
//modification_time
sqlite3_bind_int(stmt, 5, timestamp);
//last_access_time
sqlite3_bind_int(stmt, 6, timestamp);
//extended_data
sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
复制代码
框架做者用数据库的一条记录来保存关于某个缓存的全部信息。 并且数据库的第四个字段是保存缓存对应的data的,从上面的代码能够看出当filename为空和不为空的时候的处理的差异。
上面的sqlite3_stmt
能够看做是一个已经把sql语句解析了的、用sqlite本身标记记录的内部数据结构。 而sqlite3_bind_text和sqlite3_bind_int是绑定函数,能够看做是将变量插入到字段的操做。
OK,如今看完了写入缓存,咱们再来看一下获取缓存的操做:
//YYKVSorage.m
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
if (key.length == 0) return nil;
YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
if (item) {
//更新内存访问的时间
[self _dbUpdateAccessTimeWithKey:key];
if (item.filename) {
//若是有文件名,则尝试获取文件数据
item.value = [self _fileReadWithName:item.filename];
//若是此时获取文件数据失败,则删除对应的item
if (!item.value) {
[self _dbDeleteItemWithKey:key];
item = nil;
}
}
}
return item;
}
复制代码
从上面这段代码咱们能够看到获取YYKVStorageItem的实例的方法是_dbGetItemWithKey:excludeInlineData:
咱们来看一下它的实现:
来看一下代码:
- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData {
NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return nil;
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
YYKVStorageItem *item = nil;
int result = sqlite3_step(stmt);
if (result == SQLITE_ROW) {
//传入stmt来生成YYKVStorageItem实例
item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData];
} else {
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
}
}
return item;
}
复制代码
咱们能够看到最终生成YYKVStorageItem实例的是经过_dbGetItemFromStmt:excludeInlineData:
来实现的:
- (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData {
//提取数据
int i = 0;
char *key = (char *)sqlite3_column_text(stmt, i++);
char *filename = (char *)sqlite3_column_text(stmt, i++);
int size = sqlite3_column_int(stmt, i++);
//判断excludeInlineData
const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i);
int inline_data_bytes = excludeInlineData ? 0 : sqlite3_column_bytes(stmt, i++);
int modification_time = sqlite3_column_int(stmt, i++);
int last_access_time = sqlite3_column_int(stmt, i++);
const void *extended_data = sqlite3_column_blob(stmt, i);
int extended_data_bytes = sqlite3_column_bytes(stmt, i++);
//将数据赋给item的属性
YYKVStorageItem *item = [YYKVStorageItem new];
if (key) item.key = [NSString stringWithUTF8String:key];
if (filename && *filename != 0) item.filename = [NSString stringWithUTF8String:filename];
item.size = size;
if (inline_data_bytes > 0 && inline_data) item.value = [NSData dataWithBytes:inline_data length:inline_data_bytes];
item.modTime = modification_time;
item.accessTime = last_access_time;
if (extended_data_bytes > 0 && extended_data) item.extendedData = [NSData dataWithBytes:extended_data length:extended_data_bytes];
return item;
}
复制代码
上面这段代码分为两个部分:
须要注意的是:
stringWithUTF8String:
来转成NSString类型。excludeInlineData
:我相信对于某个设计来讲,它的产生必定是基于某种个特定问题下的某个场景的
由上文能够看出:
在YYMemoryCache中,是使用互斥锁来保证线程安全的。 首先在YYMemoryCache的初始化方法中获得了互斥锁,并在它的全部接口里都加入了互斥锁来保证线程安全,包括setter,getter方法和缓存操做的实现。举几个例子:
- (NSUInteger)totalCost {
pthread_mutex_lock(&_lock);
NSUInteger totalCost = _lru->_totalCost;
pthread_mutex_unlock(&_lock);
return totalCost;
}
- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread {
pthread_mutex_lock(&_lock);
_lru->_releaseOnMainThread = releaseOnMainThread;
pthread_mutex_unlock(&_lock);
}
- (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;
}
- (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;
}
复制代码
并且须要在dealloc方法中销毁这个锁头:
- (void)dealloc {
...
//销毁互斥锁
pthread_mutex_destroy(&_lock);
}
复制代码
框架做者采用了信号量的方式来给 首先在初始化的时候实例化了一个信号量:
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
_lock = dispatch_semaphore_create(1);
_queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
...
复制代码
而后使用了宏来代替加锁解锁的代码:
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
复制代码
简单说一下信号量:
dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是
当信号量为0时,就会作等待处理,这是其余线程若是访问的话就会让其等待。因此若是信号量在最开始的的时候被设置为1,那么就能够实现“锁”的功能:
须要注意的是:若是有多个线程等待,那么后来信号量恢复之后访问的顺序就是线程遇到dispatch_semaphore_wait的顺序。
这也就是信号量和互斥锁的一个区别:互斥量用于线程的互斥,信号线用于线程的同步。
互斥:是指某一资源同时只容许一个访问者对其进行访问,具备惟一性和排它性。但互斥没法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数状况),经过其它机制实现访问者对资源的有序访问。在大多数状况下,同步已经实现了互斥,特别是全部写入资源的状况一定是互斥的。也就是说使用信号量可使多个线程有序访问某个资源。
那么问题来了:为何内存缓存使用的是互斥锁(pthread_mutex),而磁盘缓存使用的就是信号量(dispatch_semaphore)呢?
答案在框架做者的文章YYCache 设计思路里能够找到:
为何内存缓存使用互斥锁(pthread_mutex)?
框架做者在最初使用的是自旋锁(OSSpinLock)做为内存缓存的线程锁,可是后来得知其不够安全,因此退而求其次,使用了pthread_mutex。
为何磁盘缓存使用的是信号量(dispatch_semaphore)?
dispatch_semaphore 是信号量,但当信号总量设为 1 时也能够看成锁来。在没有等待状况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待状况出现时,性能就会降低许多。相对于 OSSpinLock 来讲,它的优点在于等待时不会消耗 CPU 资源。对磁盘缓存来讲,它比较合适。
由于YYDiskCache在写入比较大的缓存时,可能会有比较长的等待时间,而dispatch_semaphore在这个时候是不消耗CPU资源的,因此比较适合。
能够参考上一部分YYMemoryCache 和YYDiskCache使用的不一样的锁以及缘由。
在YYMemoryCache中,做者选择了双向链表来保存这些缓存节点。那么能够思考一下,为何要用双向链表而不是单向链表或是数组呢?
为何不选择单向链表:单链表的节点只知道它后面的节点(只有指向后一节点的指针),而不知道前面的。因此若是想移动其中一个节点的话,其先后的节点很差作衔接。
为何不选择数组:数组中元素在内存的排列是连续的,对于寻址操做很是便利;可是对于插入,删除操做很不方便,须要总体移动,移动的元素个数越多,代价越大。而链表偏偏相反,由于其节点的关联仅仅是靠指针,因此对于插入和删除操做会很便利,而寻址操做缺比较费时。因为在LRU策略中会有很是多的移动,插入和删除节点的操做,因此使用双向链表是比较有优点的。
不管缓存的自动清理和释放,做者默认把这些任务放到子线程去作:
看一下释放全部内存缓存的操做:
- (void)removeAll {
//将开销,缓存数量置为0
_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);
}
}
}
复制代码
这里的YYMemoryCacheGetReleaseQueue()
使用了内联函数,返回了低优先级的并发队列。
//内联函数,返回优先级最低的全局并发队列
static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
}
复制代码
一样是字典实现,可是做者使用了更底层且快速的CFDictionary而没有用NSDictionary来实现。
YYCache有4个供外部调用的初始化接口,不管是对象方法仍是类方法都须要传入一个字符串(名称或路径)。
而两个原生的初始化方法被框架做者禁掉了:
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
复制代码
若是用户使用了上面两个初始化方法就会在编译期报错。
而剩下的四个可使用的初始化方法中,有一个是指定初始化方法,被做者用NS_DESIGNATED_INITIALIZER
标记了。
- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;
复制代码
指定初始化方法就是全部可以使用的初始化方法都必须调用的方法。更详细的介绍能够参考个人下面两篇文章:
为了异步将某个对象释放掉,能够经过在GCD的block里面给它发个消息来实现。这个技巧在该框架中很常见,举一个删除一个内存缓存的例子:
首先将这个缓存的node类取出,而后异步将其释放掉。
- (void)removeObjectForKey:(id)key {
if (!key) return;
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
[_lru removeNode:node];
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);
}
复制代码
为了释放掉这个node对象,在一个异步执行的(主队列或自定义队列里)block里给其发送了class
这个消息。不须要纠结这个消息具体是什么,他的目的是为了不编译错误,由于咱们没法在block里面硬生生地将某个对象写进去。
其实关于上面这一点我本身也有点拿不许,但愿理解得比较透彻的同窗能在下面留个言~ ^^
YYCache默认在收到内存警告和进入后台时,自动清除全部内存缓存。因此在YYMemoryCache的初始化方法里,咱们能够看到这两个监听的动做:
//YYMemoryCache.m
- (instancetype)init{
...
//监听app生命周期
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
...
}
复制代码
而后实现监听到消息后的处理方法:
//内存警告时,删除全部内存缓存
- (void)_appDidReceiveMemoryWarningNotification {
if (self.didReceiveMemoryWarningBlock) {
self.didReceiveMemoryWarningBlock(self);
}
if (self.shouldRemoveAllObjectsOnMemoryWarning) {
[self removeAllObjects];
}
}
//进入后台时,删除全部内存缓存
- (void)_appDidEnterBackgroundNotification {
if (self.didEnterBackgroundBlock) {
self.didEnterBackgroundBlock(self);
}
if (self.shouldRemoveAllObjectsWhenEnteringBackground) {
[self removeAllObjects];
}
}
复制代码
#if __has_include(<YYCache/YYCache.h>)
#import <YYCache/YYMemoryCache.h>
#import <YYCache/YYDiskCache.h>
#import <YYCache/YYKVStorage.h>
#elif __has_include(<YYWebImage/YYCache.h>)
#import <YYWebImage/YYMemoryCache.h>
#import <YYWebImage/YYDiskCache.h>
#import <YYWebImage/YYKVStorage.h>
#else
#import "YYMemoryCache.h"
#import "YYDiskCache.h"
#import "YYKVStorage.h"
#endif
复制代码
在这里做者使用__has_include来检查Frameworks是否引入某个类。 由于YYWebImage已经集成YYCache,因此若是导入过YYWebImage的话就无需重再导入YYCache了。
经过看该组件的源码,我收获的不只有缓存设计的思路,还有:
相信读过这篇文章的你也会有一些收获~ 若是能趁热打铁,下载一个YYCache源码看就更好啦~
本篇已同步到我的博客:传送门
---------------------------- 2018年7月17日更新 ----------------------------
注意注意!!!
笔者在近期开通了我的公众号,主要分享编程,读书笔记,思考类的文章。
由于公众号天天发布的消息数有限制,因此到目前为止尚未将全部过去的精选文章都发布在公众号上,后续会逐步发布的。
并且由于各大博客平台的各类限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~
扫下方的公众号二维码并点击关注,期待与您的共同成长~