iOS YYCache 源码解析 观后感

前言

开发经常会用到数据的存储,自定义对象保存等操作。下面我们来介绍下目前最火的存储框架YYCache,他的存储使用的双向链表的存储方式、线程锁、数据库等知识。希望提升下自己的能力。YYCache的使用方法我们这里就不介绍了,各位可以自行百度。

缓存架构
缓存架构图

YYCache

从YYCache源码
存数据: 当调用存储方法的时候会先写入内存缓存方法,再写入磁盘缓存。

 //先存入内存 再存入磁盘
- (void)setObject:(id)object forKey:(NSString *)key {
    [_memoryCache setObject:object forKey:key]; 
    [_diskCache setObject:object forKey:key];

}

取数据 当调用获取缓存数据的时候,会先从内存缓存中获取,如果没有,会从磁盘缓存中寻找,如果还没有,返回没有找到数据。

- (id)objectForKey:(NSString *)key {
// 内存缓存 获取
    id object = [_memoryCache objectForKey:key];
    if (!object) {
        //磁盘缓存 获取
        object = [_diskCache objectForKey:key];
        if (object) { 
            //如果内存缓存没有找到,在磁盘缓存中找到,会将磁盘取出来的数据,再次存入内存缓存,以便下次提高获取速度
            [_memoryCache setObject:object forKey:key];
        }
    }
    return object;
}

YYMemoryCache

内存缓存:特点是速度快,适用于体积小的数据存储。而且内部实现了 LRU算法 (缓存淘汰算法)。
LRU算法: 在于区分高低频率的次数,当缓存达到一定数量的时候,会优先删除那些访问频率低的数据,具体是怎么实现的呢?

  • 当写入一个新节点的时候,会将这个节点放置链表的最前端。
  • 当访问一个已经存在的节点,会将这个节点拿出来,放置在链表顶端,然后将该节点的上下两个几点链接在一起。原头部节点,会变成第二个节点。
  • 当清除缓存的时候,因为根据上述排序,排在最低端的数据是频率最低的,并且上次访问时间间隔最长。所以从链表的低端清楚数据即可。

为了便于理解,我们可以把这个抽象概念类比于幼儿园手拉手的小朋友们:每个小朋友的左手都拉着前面小朋友的右手;每个小朋友的右手都拉着后面小朋友的左手;而且最前面的小朋友的左手和最后面的小朋友的右手都没有拉任何一个小朋友。如果要其中一个小朋友放在队伍的最前面,需要
1.将原来这个小朋友前后的小朋友的手拉上。
2.然后将这个小朋友的右手和原来排在第一位的小朋友的左手拉上。

在YYMemoryCache中运用_YYLinkedMapNode 跟_YYLinkedMap两个类来实现链表的LRU算法排序


链表示意图
//节点
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev;//该节点的上个节点
    __unsafe_unretained _YYLinkedMapNode *_next; //该节点的下个节点
    id _key; //缓存的key
    id _value; //缓存的值
    NSUInteger _cost; //缓存开销
    NSTimeInterval _time; //访问时间
}
@end
//链表
@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // 用于存放节点模型
    NSUInteger _totalCost;  //该链表的总共开销
    NSUInteger _totalCount; //该链表的总缓存数量
    _YYLinkedMapNode *_head; // 链表的头部节点
    _YYLinkedMapNode *_tail; // 链表的尾部节点
    BOOL _releaseOnMainThread; //是否在主线程释放
    BOOL _releaseAsynchronously; //是否在子线程释放
}

以上是链表定义的两个类其中在 _YYLinkedMap中定义的方法来操作链表

// 插入节点放置头部
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

// 链表内部的一个节点位移到改链表头部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

// 移除某个节点
- (void)removeNode:(_YYLinkedMapNode *)node;

// 移除尾部节点
- (_YYLinkedMapNode *)removeTailNode;

// 移除所有节点
- (void)removeAll;

接下来我们分析下源码实现
将节点置于链表头部

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
    //存入模型
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
    //增加该节点开销
    _totalCost += node->_cost;
    //增加缓存数量
    _totalCount++;
    //如果有头部节点,说明该模型内部个数大于1
    if (_head) {
        node->_next = _head;//1.该节点的尾指针点指向头部节点,因为此时的_head将会是赋值后的第二个节点
        _head->_prev = node;//2.该节点的头指针指向改节点
        _head = node;       //3.重新赋值head节点,由于步骤1的缘故,之前的_head会将是第二个节点
    } else {
        //链表内没有元素,将头部尾部都指向该节点。
        _head = _tail = node;
    }
}

将某个元素移动至头部

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    if (_head == node) return; //如果已经是头部节点直接返回

    if (_tail == node) { //如果当前节点是尾节点,
        _tail = node->_prev; //1.拿出尾部节点之后, node->_prev是倒数第二个节点,此时会成为尾节点,重新赋值尾节点
        _tail->_next = nil; //2.将尾部节点的尾指针置为空,表明是最尾部
    } else {//当前节点是链表中间 拿出中间节点,需要将该节点的 左右节点链接
        node->_next->_prev = node->_prev; //1.当前节点的尾指针指向的节点 的头指针指向该节点的头指针
        node->_prev->_next = node->_next; //2.当前节点的头指针指向的节点 的尾指针指向该节点的尾指针
    }
    node->_next = _head; //将该节点的尾指针指向当前头部指针,老的头部节点会成为第二个节点
    node->_prev = nil; //高节点的头指针指向空,表明是头节点
    _head->_prev = node; //老的头部节点会成为第二个节点,重新复制头指针
    _head = node;  //重新赋值头元素
}

移除某个节点

- (void)removeNode:(_YYLinkedMapNode *)node {
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key)); //模型中移除该节点
    //减去开销
    _totalCost -= node->_cost;
    //减去缓存数
    _totalCount--;
    if (node->_next) node->_next->_prev = node->_prev; //如果当前节点尾指针不为空, 将当前节点的尾指针指向的节点  的头指针  指向该节点的头指针
    if (node->_prev) node->_prev->_next = node->_next; //如果当前节点头指针不为空, 将当前节点的头指针指向的节点  的尾指针  指向该节点的尾指针
    if (_head == node) _head = node->_next;  //如果是头节点,则需要重新赋值头节点
    if (_tail == node) _tail = node->_prev;  //如果是尾节点,则需要重新赋值尾节点
}

移除尾部节点,并返回节点

- (_YYLinkedMapNode *)removeTailNode {
    if (!_tail) return nil; //如果不是尾节点 返回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;
}

移除全部节点

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

以上就是YYMemoryCache中处理链表的相关代码,下面我们就来看下具体接口是怎么实现的。
线程锁
源码中对变量的处理大多都采用了加锁的模式,这也是一大优点。

- (NSUInteger)totalCount {
    pthread_mutex_lock(&_lock); //加锁
    NSUInteger count = _lru->_totalCount;
    pthread_mutex_unlock(&_lock); //解锁
    return count;
}

缓存某个对象

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;  //键值空 返回
    if (!object) {
        [self removeObjectForKey:key];  //值为空,删除对象的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);
}

取出某个缓存对象

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

总结:以上就是YYMemoryCache内部的实现原理,内存缓存使用了LRU淘汰机制算法,最新访问的在最顶部,访问次数最低的放置最底部,便于移除的时候移除不经常访问的缓存,还使用了双向链表来实现LRU算法的基础。

YYDiskCache

内存缓存:特点相对低速,适用于体积大的数据存储。相比较YYMemoryCache

共同点 :
1 都具有查询,写入,读取,删除缓存的接口。
2 不直接操作缓存,也是间接地通过另一个类(YYKVStorage)来操作缓存。
3 它使用 LRU 算法来清理缓存。
4 支持按 cost,count 和 age 这三个维度来清理不符合标准的缓存。
差异点:
1 根据缓存数据的大小来采取不同的形式的缓存:
数据库 sqlite: 针对小容量缓存,缓存的 data 和元数据都保存在数据库里。
文件+数据库的形式: 针对大容量缓存,缓存的 data 写在文件系统里,其元数据保存在数据库里。
2 除了 cost,count 和 age 三个维度之外,还添加了一个磁盘容量的维度。

从源码上来看,跟内存缓存的存储方式的方法大致也是相同的。YYDiskCache的储存方式有是三种,还有初始化的时候YYDiskCache有一个属性:
inlineThreshold(内联阈值),默认是20KB,超过这个值,会将使用混合存储。大于这个值,将会使用文件存储。当然你也可以自己选择存储方式

//初始化三种属性
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    
    /// The `value` is stored as a file in file system.
    YYKVStorageTypeFile = 0, //文件存储
    
    /// The `value` is stored in sqlite with blob type.
    YYKVStorageTypeSQLite = 1, //数据库存储
    
    /// The `value` is stored in file system or sqlite based on your choice.
    YYKVStorageTypeMixed = 2, //混合存储,元数据存入数据库,value存入文件
};
@property (readonly) NSUInteger inlineThreshold; 
//内联阈值,默认是20KB,超过这个值,会将使用混合存储。大于这个值,将会使用文件存储。当然你也可以自己选择存储方式
- (nullable id)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id _Nullable object))block;
- (void)setObject:(nullable id)object forKey:(NSString *)key;
- (void)setObject:(nullable id)object forKey:(NSString *)key withBlock:(void(^)(void))block;

值得一提的是,因为磁盘缓存一般会存储较大的文件类型,耗时有可能会长,所以这里面增加了block回调机制,避免阻塞线程。下面我们就来看下具体实现过程:
写入过程setObject: forKey:

- (void)setObject:(id)object forKey:(NSString *)key {
    if (!key) return;  //判断键值
    if (!object) {     //存储为空,移除对应的key值
        [self removeObjectForKey:key];
        return;
    }
    
    NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object]; //扩展数据
    NSData *value = nil;
    if (_customArchiveBlock) {
        value = _customArchiveBlock(object);
    } else { //如果自定义归档格式,实现回调
        @try {
            value = [NSKeyedArchiver archivedDataWithRootObject:object];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
    }
    if (!value) return;
    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();
}

读取过程objectForKey:

- (id)objectForKey:(NSString *)key {
    if (!key) return nil;
    Lock();
    YYKVStorageItem *item = [_kv getItemForKey:key]; //获取缓存对象
    Unlock();
    if (!item.value) return nil;
    
    id object = nil;
    if (_customUnarchiveBlock) {
        object = _customUnarchiveBlock(item.value);
    } else {
        @try { //解档对象
            object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
    }
    if (object && item.extendedData) { //追加附加数据
        [YYDiskCache setExtendedData:item.extendedData toObject:object];
    }
    return object;
}

从上述我们可以看出,操作磁盘缓存主要是利用 *YYKVStorage _kv;这个对象,这个kv是操作数据库的类,里面存储着 增删改更新等操作的方法。kv存入数据库是用YYKVStorageItem对象进行存储。
下面我就研究下这个 YYKVStorage这个类

//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 *)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 *)getItemForKeys:(NSArray *)keys;

//读取参数数组对应的item字典
- (nullable NSDictionary *)getItemValueForKeys:(NSArray *)keys;

从上面面我们可以看出写入的方法

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

我们可以看出调用了两次 _dbSaveWithKey:value:fileName:extendedData:第二次使用数据库存储的时候传入的fileName是nil

  • 当 fileName 为空时,说明在外部没有写入该缓存的文件:则把 data 写入数据库里
  • 当 fileName 不为空时,说明在外部有写入该缓存的文件:则不把 data 也写入了数据库里。
- (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]; //可以当作是一个已经把sql语句解析了的、用sqlite自己标记记录的内部数据结构。
    if (!stmt) return NO;
    
    int timestamp = (int)time(NULL);
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //绑定函数,可以看作是将变量插入到字段的操作
    sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
    sqlite3_bind_int(stmt, 3, (int)value.length);
    if (fileName.length == 0) {
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
    } else {
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
    sqlite3_bind_int(stmt, 5, timestamp);
    sqlite3_bind_int(stmt, 6, timestamp);
    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;
}

以上就是YYCache整个大致的源码流程,其中有好多优点值得我们去学习,比如

  • 双向链表的运用
  • 线程锁的运用
  • 数据库的存储
  • 等等
    以下是我从其他文档看出来的几个总结,比较好。
  1. YYMemoryCache使用的是pthread_mutex 线程锁(互斥锁)来确保线程安全,而YYDiskCache则使用了更加适合他的dispatch_semaphore(信号量)。

答:
1.互斥(互斥锁用于用于线程的互斥):是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
2.同步(信号量用于线程的同步):是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。也就是说使用信号量可以使多个线程有序访问某个资源。
3.dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
因为 YYDiskCache 在写入比较大的缓存时,可能会有比较长的等待时间,而 dispatch_semaphore 在这个时候是不消耗CPU资源的,所以比较适合。

  1. YYMemoryCache为什么要使用双向链表,而不是用单向链表或者数组呢?

答:1.单链表的节点只知道它后面的节点,当移动某个节点的时候,前后两个节点无法衔接。
2.数组内的数据是有序的,对于寻址非常方便,但是对于插入删除就非常麻烦了,需要移动剩下多个数据,移动的越多,性能就越下降,而链表不同,他的节点关联是靠指针,删除插入比较便利,但是寻址比较麻烦,但是使用LRU算法中使用哈希表来存入数据,来有效避免这个问题,所以使用双向链表是最佳的选择。

本文章参考文献:
YYCachey源码解析
YYCache源码解析
YYCache源码解析
YYCache源码

你可能感兴趣的:(iOS YYCache 源码解析 观后感)