源码解析--YYCache

封面.jpg

前言:准备看下YY系列中的YYWebImage框架,发现该框架是使用YYCache来做缓存的。那就从缓存开始吧.
先奉上YYCache框架的地址以及作者的设计思路
学习YYCache框架你可以get到:
1.优雅的代码风格
2.优秀的接口设计
3.YYCache的层次结构
4.YYMemoryCache类的层次结构和缓存机制
5.YYDiskCache类的层次结构和缓存机制

YYCache

YYCache结构.png

YYCache最为食物链的最顶端的男人,并没有什么好说的,所以我们就从YYMemoryCacheYYDiskCache开始吧。

YYMemoryCache

YYMemoryCache内存储存是的原理是利用CFDictionary对象的 key-value开辟内存储存机制和双向链表原理来实现LRU算法。这里是官方文档对CFDictionary的解释:

CFMutableDictionary creates dynamic dictionaries where you can add or delete key-value pairs at any time, and the dictionary automatically allocates memory as needed.

YYMemoryCache类结构图.png

YYMemoryCache初始化的时候会建立空的私有对象YYLinkedMap链表,接下来所有的操作其实就是对这个链表的操作。当然,YYMemoryCache提供了一个定时器接口给你,你可以通过设置autoTrimInterval属性去完成每隔一定时间去检查countLimitcostLimit是否达到了最大限制,并做相应的操作。

- (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)_trimInBackground {
    dispatch_async(_queue, ^{
        //检查是否达到设置的最大消耗,并做相应的处理
        [self _trimToCost:self->_costLimit];
        //检查是否达到该缓存设置的最大持有对象数,并做相应的处理
        [self _trimToCount:self->_countLimit];
        //当前的时间和链表最后的节点时间的差值是否大于设定的_ageLimit值,移除大于该值得节点
        [self _trimToAge:self->_ageLimit];
    });
}

YYMemoryCache以block的形式给你提供了下面接口:

  • didReceiveMemoryWarningBlock(当app接受到内存警告)
  • didEnterBackgroundBlock (当app进入到后台)

当然,你也可以通过设置相应的shouldRemoveAllObjectsOnMemoryWarningshouldRemoveAllObjectsWhenEnteringBackground值来移除YYMemoryCache持有的链表。

下面我们来看看YYMemoryCache类的增,删,查等操作。在这之前我们先看看YYLinkedMap这个类。

1.YYLinkedMap内部结构

YYLinkedMap作为双向链表,主要的工作是为YYMemoryCache类提供对YYLinkedMapNode节点的操作。下图绿色部分代表节点:

双向链表结构.png

下图是链表节点的结构图:
链表节点.png

现在我们先来看如何去构造一个链表添加节点:
setObject.png

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    //锁
    pthread_mutex_lock(&_lock);
    //查找是否存在对应该key的节点
    _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算法原理,将访问的点移到最前面
        [_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);
}

你可以点击这里自己去操作双向链表

addNode.gif

链表移除节点的操作:

- (void)removeObjectForKey:(id)key {
    if (!key) return;
    //锁
    pthread_mutex_lock(&_lock);
    //根据key拿到相应的节点
    _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);
}

removeNode.gif

YYMemoryCache类还为我们提供了下列接口方便我们调用:

- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)removeAllObjects;

总结:YYMemoryCache是利用key-value机制内存缓存类,所有的方法都是线程安全的。如果你熟悉NSCache类,你会发现两者的接口很是相似。
当然YYMemoryCache有着自己的特点:
1.YYMemoryCache采用LRU(least-recently-used)算法来移除节点。
2.YYMemoryCache可以用countLimitcostLimitageLimit属性做相应的控制。
3.YYMemoryCache类可以设置相应的属性来控制退到后台或者接受到内存警告的时候移除链表。

YYKVStorage

YYKVStorage是一个基于sql数据库和文件写入的缓存类,注意它并不是线程安全。你可以自己定义YYKVStorageType来确定是那种写入方式:

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

1.写入和更新

我们看看Demo中直接用YYKVStorage储存NSNumber和NSData YYKVStorageTypeFileYYKVStorageTypeSQLite类型所用的时间:

7.png

你可以发现在储存小型数据NSNumberYYKVStorageTypeFile类型是YYKVStorageTypeSQLite大约4倍多,而在大型数据的时候两者的表现是相反的。显然选择合适的储存方式是很有必要的。
这里需要提醒的事:

  • DemoYYKVStorageTypeFile类型其实不仅写入了本地文件也同时写入了数据库,只不过数据库里面存的是除了value值以外的key, filename, size, inline_data(NULL), modification_time , last_access_time, extended_data字段。
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    if (key.length == 0 || value.length == 0) return NO;
    //_type为YYKVStorageTypeSQLite时候filename应该为空,不然还是会写入文件
    //_type为YYKVStorageTypeFile时候filename的值不能为空
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }
    //是否写入文件是根据filename.length长度来判断的
    if (filename.length) {
        //先储存在文件里面
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        //储存在sql数据库
        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];
            }
        }
        //储存在sql数据库
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

插入或者是更新数据库

- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
    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);
    //sqlite3_bind_xxx函数给这条语句绑定参数
    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);
    //当fileName为空的时候存在数据库的是value.bytes,不然存的是NULl对象
    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);
    //通过sqlite3_step命令执行创建表的语句
    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;
}

2.读取

我们尝试的去缓存里面拿取数据,我们发现当为YYKVStorage对象type不同,存取的方式不同所以读取的方式也不同:
1.因为在插入的时候我们就说了,当为YYKVStorageTypeFile类型的时候数据是存在本地文件的其他存在数据库。所以YYKVStorage对象先根据key从数据库拿到数据然后包装成YYKVStorageItem对象,然后再根据filename读取本地文件数据赋给YYKVStorageItem对象的value属性。
2.当为YYKVStorageTypeSQLite类型就是直接从数据库把所有数据都读出来赋给YYKVStorageItem对象。

- (YYKVStorageItem *)getItemForKey:(NSString *)key {
    if (key.length == 0) return nil;
    /*先从数据库读包装item,
     当时filename不为空的时候,以为着数据库里面没有存Value值,还得去文件里面读出来value值
     当时filename为空的时候,意味着直接从数据库来拿取Value值
     */
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
    if (item) {
        //更新的last_access_time字段
        [self _dbUpdateAccessTimeWithKey:key];
        if (item.filename) {
            //从文件里面读取value值
            item.value = [self _fileReadWithName:item.filename];
            if (!item.value) {
                //数据为空则从数据库删除这条记录
                [self _dbDeleteItemWithKey:key];
                item = nil;
            }
        }
    }
    return item;
}

3.删除

YYKVStorage的type当为YYKVStorageTypeFile类型是根据key将本地和数据库都删掉,而YYKVStorageTypeSQLite是根据key删除掉数据库就好了。

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

我们这里分别列取了增删改查的单个key的操作,你还可以去批量的去操作key的数组。但是其实都大同小异的流程,就不一一累述了。上个图吧:

屏幕快照 2016-12-28 下午10.10.38.png

这个类也就看的差不多了,但是要注意的事,YYCache作者并不希望我们直接使用这个类,而是使用更高层的YYDiskCache类。那我们就继续往下面看吧。

YYDiskCache

YYDiskCache类有两种初始化方式:

- (nullable instancetype)initWithPath:(NSString *)path;
- (nullable instancetype)initWithPath:(NSString *)path
                      inlineThreshold:(NSUInteger)threshold 

YYDiskCache类持有一个YYKVStorage对象,但是你不能手动的去控制YYKVStorage对象的YYKVStorageTypeYYDiskCache类初始化提供一个threshold的参数,默认的为20KB。然后根据这个值得大小来确定YYKVStorageType的类型。

YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];

因为YYDiskCache类的操作其实就是去操作持有的YYKVStorage对象,所以下面的部分会比较建简略。

写入和更新

在调用YYKVStorage对象的储存操作前主要做了下面几项操作:
1.key和object的判空容错机制
2.利用runtime机制去取extendedData数据
3.根据是否定义了_customArchiveBlock来判断选择序列化object还是block回调得到value
4.value的判空容错机制
5.根据YYKVStorage的type判断以及_inlineThreshold和value值得长度来判断是否选择以文件的形式储存value值。上面我们说过当value比较大的时候文件储存速度比较快速。
6.如果_customFileNameBlock为空,则根据key通过md5加密得到转化后的filename.不然直接拿到_customFileNameBlock关联的filename。生成以后操作文件的路径
做完上面的操作则直接调用YYKVStorage储存方法,下面是实现代码:

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    //runtime 取extended_data_key的value
    NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
    NSData *value = nil;
    if (_customArchiveBlock) {
        //block返回
        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) {
        //长度判断这个储存方式,value.length当大于_inlineThreshold则文件储存
        if (value.length > _inlineThreshold) {
            //将key 进行md5加密
            filename = [self _filenameForKey:key];
        }
    }
    
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}

读取

读取操作一般都是和写入操作相辅相成的,我们来看看在调用YYKVStorage对象的读取操作后做了哪些操作:
1.item.value的判空容错机制
2.根据_customUnarchiveBlock值来判断是直接将item.value block回调还是反序列化成object
3.根据object && item.extendedData 来决定是否runtime添加extended_data_key属性

- (id<NSCoding>)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对象来操作了。

- (void)removeObjectForKey:(NSString *)key {
    if (!key) return;
    Lock();
    [_kv removeItemForKey:key];
    Unlock();
}

当然,YYDiskCacheYYMemoryCache一样也给你提供了一些类似limit的接口供你操作。

- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;

YYKVStorage不一样的是,作为更高层的YYDiskCache是一个线程安全的类。你应该使用YYDiskCache而不是YYKVStorage

最后再带一笔食物端最顶端的男人YYCache,当他写入的时候会同时调用YYDiskCache磁盘操作和YYMemoryCache内存操作。读取的时候先从内存读取,因为在内存的读取速度比磁盘快很多,如果没有读取到数据才会去磁盘读取。

读后感只有四个字:

如沐春风

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

推荐阅读更多精彩内容