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

2021-02-05  本文已影响0人  无悔zero

之前分析过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访问时间。因为缓存对象添加时会放在最前面,淘汰缓存时淘汰最后一个,所以说释放缓存时,会优先淘汰最近最少使用的对象。

上一篇下一篇

猜你喜欢

热点阅读