美文网首页
YYKit源码分析(3)——YYCache缓存

YYKit源码分析(3)——YYCache缓存

作者: 无悔zero | 来源:发表于2021-02-05 18:58 被阅读0次

    之前分析过NSCache,现在就来看看YYCache是怎么做缓存的。先看个例子:

    Person *person = [[Person alloc]init];
    person.name = @"你的名字";
    
    YYCache *cache = [[YYCache alloc]initWithName:@"PersonCache"];//创建数据库等
    [cache setObject: person  forKey:@"key"];
    
    1. YYCache初始化就是创建数据库等一些准备工作,我们重点看[setObject:forKey:]
    @implementation YYCache
    ...
    - (instancetype)initWithName:(NSString *)name {
        if (name.length == 0) return nil;
        NSString *cacheFolder = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
        NSString *path = [cacheFolder stringByAppendingPathComponent:name];
        return [self initWithPath:path];
    }
    ...
    - (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
        [_memoryCache setObject:object forKey:key];//内存
        [_diskCache setObject:object forKey:key];//磁盘
    }
    
    1. 先来看YYMemoryCache
    @implementation YYMemoryCache
    ...
    - (void)setObject:(id)object forKey:(id)key {
        [self setObject:object forKey:key withCost:0];
    }
    
    @implementation YYMemoryCache
    ...
    - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
        ...
        pthread_mutex_lock(&_lock);//安全锁
        _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
        NSTimeInterval now = CACurrentMediaTime();
        //双向链表节点保存
        if (node) {
            ...
            [_lru bringNodeToHead:node];//放到最前面
        } else {
            node = [_YYLinkedMapNode new];
            ...
            [_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()) { ... }
        }
        pthread_mutex_unlock(&_lock);
    }
    

    缓存结构跟NSCache一样也是双向链表节点结构:

    @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
    
    @implementation _YYLinkedMap
    ...
    - (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
        CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
        _totalCost += node->_cost;
        _totalCount++;
        if (_head) {
            //放到当前head前面
            node->_next = _head;
            _head->_prev = node;
            _head = node;
        } else {
            _head = _tail = node;//没有head代表只有一个
        }
    }
    
    - (void)bringNodeToHead:(_YYLinkedMapNode *)node {
        if (_head == node) return;
        //区分尾部和中间处理
        if (_tail == node) {
            _tail = node->_prev;
            _tail->_next = nil;
        } else {
            node->_next->_prev = node->_prev;
            node->_prev->_next = node->_next;
        }
        //放到当前head前面
        node->_next = _head;
        node->_prev = nil;
        _head->_prev = node;
        _head = node;
    }
    
    1. 然后先根据_costLimit释放溢出缓存,并利用子线程进行释放(详细释放原理可看YYImage源码分析的补充):
    @implementation YYMemoryCache
    ...
    - (_YYLinkedMapNode *)removeTailNode {
        ...
        CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key));//移除最后一个
        _totalCost -= _tail->_cost;
        _totalCount--;
        ...
        return tail;//返回
    }
    ...
    - (void)_trimToCost:(NSUInteger)costLimit {
        ...
        NSMutableArray *holder = [NSMutableArray new];
        while (!finish) {
            if (pthread_mutex_trylock(&_lock) == 0) {
                if (_lru->_totalCost > costLimit) {
                    _YYLinkedMapNode *node = [_lru removeTailNode];//移除返回
                    if (node) [holder addObject:node];//添加
                } else {
                    finish = YES;
                }
                pthread_mutex_unlock(&_lock);
            } else { ... }
        }
        if (holder.count) {
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                [holder count]; // release in queue 利用子线程释放
            });
        }
    }
    ...
    - (void)trimToCost:(NSUInteger)cost {
        [self _trimToCost:cost];
    }
    
    1. 然后继续第2步,根据_countLimit释放多出缓存对象。

    2. 接着来看YYDiskCache

    @implementation YYDiskCache
    ...
    - (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
        ...
        if (_customArchiveBlock) {
            value = _customArchiveBlock(object);//自定义归档
        } else {
            @try {
                value = [NSKeyedArchiver archivedDataWithRootObject:object];//归档
            } @catch (NSException *exception) { ... }
        }
        if (!value) return;
        NSString *filename = nil;
        if (_kv.type != YYKVStorageTypeSQLite) {
            if (value.length > _inlineThreshold) {//判断缓存大小是否大于20KB
                filename = [self _filenameForKey:key];//MD5 获取名称
            }
        }
        
        Lock();
        [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
        Unlock();
    }
    

    如果数据大于20KB,就会保存为文件,并通过MD5命名文件:

    @implementation YYDiskCache
    ...
    - (NSString *)_filenameForKey:(NSString *)key {
        NSString *filename = nil;
        if (_customFileNameBlock) filename = _customFileNameBlock(key);
        if (!filename) filename = key.md5String;
        return filename;
    }
    
    1. 然后进行保存:
    @implementation YYKVStorage
    ...
    - (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
        ...
        //根据filename判断保存方式
        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 {
            ...
            return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];//保存数据库
        }
    }
    
    1. 如果有filename就把数据保存为文件:
    @implementation YYKVStorage
    ...
    - (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data {
        NSString *path = [_dataPath stringByAppendingPathComponent:filename];
        return [data writeToFile:path atomically:NO];
    }
    

    然后数据库保存其他数据,filename作为标识和路径把文件关联起来:

    @implementation YYKVStorage
    ...
    - (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);";//sqlite保存为YYKVStorageItem
        sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
        ... //详情略
        return YES;
    }
    

    如果没有filename,就直接全部数据保存到数据库。

    1. 最后我们来看看获取:
    @implementation YYCache
    ...
    - (id<NSCoding>)objectForKey:(NSString *)key {
        id<NSCoding> object = [_memoryCache objectForKey:key];//从内存获取
        if (!object) {
            object = [_diskCache objectForKey:key];//从磁盘获取
            if (object) {
                [_memoryCache setObject:object forKey:key];//保存到内存
            }
        }
        return object;
    }
    
    1. 先从内存中获取:
    @implementation YYMemoryCache
    ...
    - (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;
    }
    
    1. 找不到再从磁盘中获取:
    @implementation YYDiskCache
    ...
    - (id<NSCoding>)objectForKey:(NSString *)key {
        if (!key) return nil;
        Lock();
        YYKVStorageItem *item = [_kv getItemForKey:key];//从数据库读取
        ...
        if (_customUnarchiveBlock) {
            object = _customUnarchiveBlock(item.value);//自定义解档
        } else {
            @try {
                object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];//解档
            } @catch (NSException *exception) { ... }
        }
        ...
        return object;
    }
    
    @implementation YYKVStorage
    ...
    - (YYKVStorageItem *)getItemForKey:(NSString *)key {
        if (key.length == 0) return nil;
        YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
        if (item) {
            [self _dbUpdateAccessTimeWithKey:key];
            if (item.filename) {//如果有filename关联
                item.value = [self _fileReadWithName:item.filename];//读取文件
                ...
            }
        }
        return item;
    }
    
    @implementation YYKVStorage
    ...
    - (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData {
        NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;";//sqlite查找
        sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
        ... //详情略
        return item;
    }
    
    1. 如果有filename,就去读取文件数据,接着回到第10步解档:
    @implementation YYKVStorage
    ...
    - (NSData *)_fileReadWithName:(NSString *)filename {
        NSString *path = [_dataPath stringByAppendingPathComponent:filename];
        NSData *data = [NSData dataWithContentsOfFile:path];
        return data;
    }
    
    1. 最终从磁盘获取后,便保存到缓存中。

    YYCache的缓存策略除了有cost缓存大小、count缓存数量,还有age访问时间。因为缓存对象添加时会放在最前面,淘汰缓存时淘汰最后一个,所以说释放缓存时,会优先淘汰最近最少使用的对象。

    相关文章

      网友评论

          本文标题:YYKit源码分析(3)——YYCache缓存

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