源码解析--YYCache

作者: 爆炸头的波波安 | 来源:发表于2016-12-29 16:40 被阅读4631次
    封面.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内存操作。读取的时候先从内存读取,因为在内存的读取速度比磁盘快很多,如果没有读取到数据才会去磁盘读取。

    读后感只有四个字:

    如沐春风

    相关文章

      网友评论

      本文标题:源码解析--YYCache

      本文链接:https://www.haomeiwen.com/subject/gabsvttx.html