YYCache源码解读

YYCache是一个线程安全的高性能键值缓存组件,代码质量很高,他的作者是国内开发者ibireme开发并且开源的. 下面就来简单剖析一下他的源码.如有哪里错误希望拍砖!多多交流多多进步!谢谢!!!

基本使用方法


//要缓存的对象
NSString*name =@"JJCoderMa";
//需要缓存的对象在缓存里对应的键
NSString *key =@"username";
//创建一个YYCache实例:userInfoCache
YYCache *userInfoCache = [YYCache cacheWithName:@"userInfocache"];
//存入键值对
[userInfoCache setObject:userName forKey:key withBlock:^{
    NSLog(@"caching object succeed");
}];

//判断缓存是否存在
[userInfoCache containsObjectForKey:key withBlock:^(NSString* _Nonnull key,BOOLcontains) {
  if(contains){
    NSLog(@"存在!");
  }
}];

//根据key读取数据
[userInfoCache objectForKey:key withBlock:^(NSString* _Nonnull key,id  _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:^(intremovedCount,inttotalCount) {
    NSLog(@"remove all cache objects: removedCount :%d  totalCount : %d",removedCount,totalCount);
} endBlock:^(BOOLerror) {
if(!error){
    NSLog(@"remove all cache objects: succeed");
}else{
      NSLog(@"remove all cache objects: failed");
}
}];

```swift

YYCache 架构图: (第一次尝试使用MindNode,图画的不好~~~~)

yycache.png
  • YYCache:提供了最外层的接口,调用了YYMemoryCache与YYDiskCache的相关方法。
  • YYMemoryCache:负责处理容量小,相对高速的内存缓存。线程安全,支持自动和手动清理缓存等功能。
  • _YYLinkedMap:YYMemoryCache使用的双向链表类。
  • _YYLinkedMapNode:是_YYLinkedMap使用的节点类。
  • YYDiskCache:负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作,自动和手动清理缓存等功能。
  • YYKVStorage:YYDiskCache的底层实现类,用于管理磁盘缓存。
  • YYKVStorageItem:内置在YYKVStorage中,是YYKVStorage内部用于封装某个缓存的类
YYCache给用户提供所有最外层的缓存操作接口,而这些接口的内部内部实际上是调用了YYMemoryCache和YYDiskCache对象的相关方法。

因为YYMemoryCache和YYDiskCache的实例作为YYCache的两个公开的属性,所以用户无法直接使用YYMemoryCache和YYDiskCache对象,只能通过属性的方式来间接使用它们。

YYCache的部分属性和接口

interface.png

YYCache的接口实现

imp.png

从上面的接口实现可以看出:在YYCache中,永远都是先访问内存缓存,然后再访问磁盘缓存(包括了写入,读取,查询,删除缓存的操作)。而且关于内存缓存(_memoryCache)的操作,是不存在block回调的

YYMemoryCache

YYMemoryCache 操作类似于NSCache,它将需要缓存的对象与传入的key关联起来。
YYMemoryCache的内部有:
缓存淘汰算法:使用LRU(least-recently-used) 算法来淘汰(清理)使用频率较低的缓存。
缓存清理策略:使用三个维度来标记,分别是count(缓存数量),cost(开销),age(距上一次的访问时间)。YYMemoryCache提供了分别针对这三个维度的清理缓存的接口。用户可以根据不同的需求(策略)来清理在某一维度超标的缓存。
缓存淘汰算法的目的在于区分出使用频率高和使用频率低的缓存,当缓存数量达到一定限制的时候会优先清理那些使用频率低的缓存。因为使用频率已经比较低的缓存在将来的使用频率也很有可能会低。

YYMemoryCache用一个链表节点类来保存某个单独的内存缓存的信息(键,值,缓存时间等),然后用一个双向链表类来保存和管理这些节点。这两个类的名称分别是:
_YYLinkedMapNode:链表内的节点类,可以看做是对某个单独内存缓存的封装。
_YYLinkedMap:双向链表类,用于保存和管理所有内存缓存(节点)
_YYLinkedMapNode可以被看做是对某个缓存的封装:它包含了该节点上一个和下一个节点的指针,以及缓存的key
和对应的值(对象),还有该缓存的开销和访问时间。

/**
 A node in linked map.
 Typically, you should not use this class directly.
 链表内的节点类,可以看做是对某个单独内存缓存的封装
 */
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end

_YYLinkedMap:

@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,用于保存节点的键值对,它还持有了链表内节点的总开销,总数量,头尾节点等数据。

[图片上传失败...(image-875de1-1522245469574)]

_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;
}

查找/缓存/更新某个对象

//是否包含某个缓存对象
- (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);
}

YYDiskCache

YYDiskCache处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作。作为YYCache的第二级缓存,

与第一级缓存YYMemoryCache的相同点是:

  • 都具有查询,写入,读取,删除缓存的接口。
  • 不直接操作缓存,也是间接地通过另一个类(YYKVStorage)来操作缓存。
  • 它使用LRU算法来清理缓存。
  • 支持按 cost,count 和 age 这三个维度来清理不符合标准的缓存。

它与YYMemoryCache不同点是:

  • 根据缓存数据的大小来采取不同的形式的缓存:
  • 数据库sqlite: 针对小容量缓存,缓存的data和元数据都保存在数据库里。
  • 除了 cost,count 和 age 三个维度之外,还添加了一个磁盘容量的维度。
// 缓存方式
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    YYKVStorageTypeFile = 0,
    YYKVStorageTypeSQLite = 1,
    YYKVStorageTypeMixed = 2,
};

缓存的大致逻辑

  1. 首先判断传入的key和value是否符合要求,如果不符合要求,则立即返回NO,缓存失败。
  2. 再判断是否type==YYKVStorageTypeFile并且文件名为空字符串(或nil):如果是,则立即返回NO,缓存失败。

判断filename是否为空字符串:

  1. 如果不为空:
    写入文件,并将缓存的key,等信息写入数据库,但是不将key对应的data写入数据库。
  2. 如果为空:
    如果缓存类型为YYKVStorageTypeSQLite:将缓存文件删除
    如果缓存类型不为YYKVStorageTypeSQLite:则将缓存的key和对应的data等其他信息存入数据库。

YYKVStorage

YYKVStorage实例负责保存和管理所有磁盘缓存。和YYMemoryCache里面的_YYLinkedMap将缓存封装成节点类_YYLinkedMapNode类似,YYKVStorage也将某个单独的磁盘缓存封装成了一个类,这个类就是YYKVStorageItem,它保存了某个缓存所对应的一些信息(key, value, 文件名,大小等等):

YYKVStorageItem结构包含了键/值/文件名/值大小/时间戳/扩展数据等等字段

/**
 YYKVStorageItem is used by `YYKVStorage` to store key-value pair and meta data.
 Typically, you should not use this class directly.
 YYKVStorage也将某个单独的磁盘缓存封装成了一个类,这个类就是YYKVStorageItem,它保存了某个缓存所对应的一些信息(key, value, 文件名,大小等等)
 */
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key;                ///< key
@property (nonatomic, strong) NSData *value;                ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size;                             ///< value's size in bytes
@property (nonatomic) int modTime;                          ///< modification unix timestamp
@property (nonatomic) int accessTime;                       ///< last access unix timestamp
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end

还有一些方法来操作数据

//YYKVStorage.h
- (BOOL)saveItem:(YYKVStorageItem *)item;
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
- (BOOL)saveItemWithKey:(NSString *)key
                  value:(NSData *)value
               filename:(nullable NSString *)filename
           extendedData:(nullable NSData *)extendedData;
#pragma mark - Remove Items
- (BOOL)removeItemForKey:(NSString *)key;
- (BOOL)removeItemForKeys:(NSArray<NSString *> *)keys;
- (BOOL)removeItemsLargerThanSize:(int)size;
- (BOOL)removeItemsEarlierThanTime:(int)time;
- (BOOL)removeItemsToFitSize:(int)maxSize;
- (BOOL)removeItemsToFitCount:(int)maxCount;

总结一下:

1. 为什么使用双向链表?
  1. 双向链表可以知道前后节点,所以如果想移动其中一个节点的话,其前后的节点不好做衔接。
  2. 其节点的关联仅仅是靠指针,所以对于插入和删除操作会很便利,而类似数组的方式寻址操作缺比较费时。由于在LRU策略中会有非常多的移动,插入和删除节点的操作,使用双向链表是比较有优势的。
2. 使用CFDictionary而没有用NSDictionary来实现?

CFDictionary 更加底层,更快.

3. 为什么内存缓存使用互斥锁?而磁盘缓存使用信号量?

作者在最初使用的是自旋锁(OSSpinLock)作为内存缓存的线程锁,但是后来得知其不够安全,所以退而求其次,使用了pthread_mutex 这篇文章有说明
网上说:但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。

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

推荐阅读更多精彩内容

  • 一个人静静在阳台上,两手双靠拢,下巴放在手臂上,看着这一片绿色的花草树木,心里很平静。 寂静的晚上一个人仰望星空,...
    周心愿阅读 313评论 0 0
  • 背景 在日常开发的一些业务场景中,如果涉及到一些敏感信息(如:付款的二维码或条形码等),而我们不希望相关敏感信息被...
    Daved阅读 27,179评论 11 18
  • 2017~11~16 信阳市七中 学校教育是“叶”的教育,家庭教育才是“根”的教育。真正的家庭教育是...
    善默勤容阅读 642评论 0 0