用心阅读 YYCache 并改装

用心阅读 YYCache 并改装

架构

YYKIT 分为内存缓存和 磁盘缓存,磁盘缓存又分为文件缓存和数据库缓存,作者认为,大于20K应该使用文件缓存,否则sqlite缓存

YYMemoryCache

YYMemoryCache 是内存缓存,速度最快,使用的是 LRU 思想和双向链表数据结构来实现的

LRU 淘汰算法是淘汰最近最少使用的数据,那么利用双向链表完全可以实现,当有新数据插入的时候,就把新数据插入到链表的头部,当使用了内存中已经存在的数据的时候,就把这条数据移动到链表的头部,然后把这个节点的前驱节点和后继节点接到一起。可以看代码,

_YYLinkedMapNode

源码里面有这样的一个类 _YYLinkedMapNode ,他其实就是链表的节点,每一个节点都有自己的前驱节点和后继节点,还有自己的数据,YYMemoryCache 除了这个之外还存放了每一个节点占用的开销大小,不过其实 YYMemoryCache 没有用到这个开销大小,像 SDWebImage 里面就用到了这个开销大小,存放图片的时候以图片的像素大小堆起来的开销大小,不过算法思想代码已经写好了,至于这个开销大小怎么来定义,开发者可以自己定义,还有一个 time,也就是时间, YYMemoryCache 也可以根据时间,淘汰旧的数据。

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

_YYLinkedMap

这个类其实就是双向链表,

{
    @package
    CFMutableDictionaryRef _dic; // do not set object directly
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU, do not change it directly
    _YYLinkedMapNode *_tail; // LRU, do not change it directly
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

dic 指的就是链表,链表中存放每一个 节点,就是 _YYLinkedMapNode , _totalCost 指的是总的开销大小,_totalCount 指的是总的缓存数量,_head 是链表的头结点,_tail 是链表的尾节点,_releaseOnMainThread 是否在主线程中释放,_releaseAsynchronously 在子线程中释放,这个之后会讲到,这个地方的处理,我个人觉得真的是太牛逼了,想了好一会才想明白,可想 YY 大神真的是牛逼,思想也牛逼。

内部的几个接口,这个需要有点链表基础,懂点就能看懂代码



/// Insert a node at head and update the total cost.
/// Node and node.key should not be nil.
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

/// Bring a inner node to header.
/// Node should already inside the dic.
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

/// Remove a inner node and update the total cost.
/// Node should already inside the dic.
- (void)removeNode:(_YYLinkedMapNode *)node;

/// Remove tail node if exist.
- (_YYLinkedMapNode *)removeTailNode;

/// Remove all node in background queue.
- (void)removeAll;

节点的插入删除没什么说的,自己看代码,如果看不懂,去看看双向链表的节点插入删除再回来看这个。

YYMemoryCache

YYMemoryCache是线程安全的,作者通过互斥锁pthread_mutex_t来控制的线程安全

内部有一个递归算法,来每隔一段时间来清理缓存,主要分为以开销大小清理和数量大小清理


- (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];
    });
}

他们都是通过检查尾节点是否符合删除的要求来做的。

详细可以看代码,都可以看懂.

主要的几个方法

- (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) {
        _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;
        [_lru insertNodeAtHead:node];
    }
    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);
}

当吧数据放到缓存中的时候,首先会检查缓存中是否已经存在这个节点,如果存在的话,那么就修改数据并且把这个节点移动到链表的头部,这个节点就视为常用数据,如果不存在就创建一个节点,并赋值,插入到链表的头部,如果在插入的过程中发现超过了开销的大小或者数量的大小,就用LRU算法来清理数据,这里指的一提的是节点的释放

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

我们都知道,局部变量在过了作用域之后会被释放,有系统释放,首先将释放的节点赋值给了局部变量 _YYLinkedMapNode *node = [_lru removeTailNode];,node 就为持有者,如果我们不做操作,那么系统就会在当前节点来释放节点,也就是在当前主线程去释放节点,坐着这里实在另一个block里面取操作了这个拥有者,这个block就拥有了这个局部变量,那么过了作用域之后,这个节点也不会释放,引用计数减一但不为0,在block里面作为任务之后,再减一,这时候就被释放了,就做到了在子线程异步释放的目的,这是我个人理解,个人感觉实在是高,高,实在是高,对于我这种菜鸟来说实在是膜拜。

YYDiskCache

先看初始化方法


- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    self = [super init];
    if (!self) return nil;
    
    YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if (globalCache) return globalCache;
    
    YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
    if (!kv) return nil;
    
    _kv = kv;
    _path = path;
    _lock = dispatch_semaphore_create(1);
    _queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
    _inlineThreshold = threshold;
    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _freeDiskSpaceLimit = 0;
    _autoTrimInterval = 60;
    
    [self _trimRecursively];
    _YYDiskCacheSetGlobal(self);
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
    return self;
}

如果阀值设置为0,那么所有的磁盘缓存都以文件的形式存储,如果设置为最大,那么就以sqlite的形式存储,否则介于两者之间,自动决定,也就是作者认为 20k 应该以文件的形式,否则数据库。里面的接口,作者都给出了同步方法和异步方法,开发者可以根据自己的需求选择使用,用到的线程队列也是 YY 大神自己设计的YYDispatchQueuePool,我的二次封装改装并开发了几个接口DispatchQueuePool.

YYKVStorage

是磁盘缓存,里面创建了 manifest 的表,表里面有


key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob

一下几个字段,分别为 key ,文件名,大小,文件的二进制数据,修改时间,访问时间和附加二进制数据,sqlite 是通过 sqlite3_wal_checkpoint 的形式,启用 wal 模式之后,改写操作是附加到 wal 文件中的,而不是直接改动数据库文件,所以数据库文件可以同时被读取,执行sqlite3_wal_checkpoint的时候,wal 文件被写到数据库文件当中,磁盘缓存,如果选择的缓存策略不是纯sqlite缓存那么就一定会涉及到文件缓存,那么在入口的时候,就会判断一下,然后传入需要缓存的文件名字,如果传入了文件名字,那么数据会被写入到文件,并且数据库中不会存数据的二进制数据,只会存文件名字,如果没有文件名字,那么会把数据的整个二进制数据存储到数据库当中,如果存储方式是数据库的形式,首先从数据库搜索该数据的key是否对应有文件名,如果有的话就去磁盘把文件名对应的文件删除,因为只存数据库,否则的话存储到数据库当中,代码

- (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];
            }
        }
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

存储的对象这样生成二进制数据?

作者是通过街归档来形成的,

@try {
            value = [NSKeyedArchiver archivedDataWithRootObject:object];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
        

当开发者可以自己定义街归档,作者给出了 block

value = _customArchiveBlock(object);

例如


[cache.diskCache setCustomArchiveBlock:^NSData * _Nonnull(id  _Nonnull object) {
        NSString *name = [NSString stringWithFormat:@"luban"];
        return [name dataUsingEncoding:NSUTF8StringEncoding];
    }];
    [cache.diskCache setCustomUnArchiveBlock:^id _Nonnull(NSData * _Nonnull data) {
        RCUserInfo *userInfo = [RCUserInfo new];
        userInfo.name = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        userInfo.address = @"召唤师峡谷";
        userInfo.age = 18;
        return userInfo;
    }];
    

删除数据

- (BOOL)removeItemForKey:(NSString *)key {
    if (key.length == 0) return NO;
    switch (_type) {
        case YYKVStorageTypeSQLite: {
            return [self _dbDeleteItemWithKey:key];
        } break;
        case YYKVStorageTypeFile:
        case YYKVStorageTypeMixed: {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
            return [self _dbDeleteItemWithKey:key];
        } break;
        default: return NO;
    }
}

如果是数据库存储那么直接删除数据库数据就好,否则先查一下文件名字,然后删除磁盘文件,最后删除数据库的数据。

还有几个方法就不一一解释了,比如删除超过指定大小的文件,删除过旧的文件,都是先从数据库搜索符合条件的数据然后删除,看代码都可以明白。

我得小改装 RCCache

因为YYCache 已经很完美了,个人觉得,所以我只是改了他的内存缓存的一点接口,也不能说是改,就是封装了一下,更加面向OC,比如之前的插入节点,


- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
    _totalCost += node->_cost;
    _totalCount++;
    if (_head) {
        node->_next = _head;
        _head->_prev = node;
        _head = node;
    } else {
        _head = _tail = node;
    }
}

我得


/**
 头插法

 @param node 插入的节点
 */
-(void)insertNodeToHead:(RCCacheLinkNode *)node{
    CFDictionarySetValue(_linkDic, (__bridge const void *)(node.key), (const void *)node);
    [self changeTheCountAndTheCost:node match:@"+"];
    if (_head) {
        node.nextEqualTo(_head);
        _head.prevEqualTo(node);
        _head = node;
        
    } else {
        _head = _tail = node;
    }
}

/**
 将节点切换到头部

 @param node 需要变换的节点
 */
-(void)bringNodeToHead:(RCCacheLinkNode *)node{
    if (_head == node) {
        return;
    }
    if (_tail == node) {
        _tail = node.prev;
        _tail.nextEqualTo(nil);
    } else {
        node.next.prevEqualTo(node.prev);
        node.prev.nextEqualTo(node.next);
    }
    node.nextEqualTo(_head);
    node.prevEqualTo(nil);
    _head.prevEqualTo(node);
    _head = node;
}

但核心还是 YY 大神的 没有变过

总结

YY 大神太牛了,有时间大家一定要看看他的开源代码

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

推荐阅读更多精彩内容