YYCache研究

693 阅读12分钟

前言

YYCache是一款非常优秀的缓存框架,完全可以替代系统NSCache使用,且还拥有本地缓存功能和自动清理功能,可以说能让你爱不释手

YYCache的作者 ibireme,其对YYCache与其他缓存组件的对比图

fe8dbb83e6d6bc9c37011fc11550959a.webp

c35f80f2df6582318c0bd99ec1ccecd2.webp

YYCache简介

其整体架构图如下所示:

image.png

YYCache:通过同时调度YYMemoryCache和YYDiskCache来实现,快速存取,使用YMemoryCache找到指定对象时,如果没找到,通过YYDiskCache到磁盘查找,找到并加入到YMemoryCache中去

YYMemoryCache:快速缓存管理对象,通过字典和双向链表来保证读取和增删的快速性,并及时通过LRU算法清理掉最久未使用的快速缓存,清理间隔默认5s

_YYLinkedMap、_YYLinkedMapNode:其为双向列表对象和,双向列表节点对象,在YYMemoryCache中使用,以保证删减过程能达到 O(n),配合字典的读取,效率有了极大提高

YYDiskCache:磁盘缓存管理对象,通过LRU算法调用YYKVStorage等,清理掉最久未使用的缓存,清理间隔默认一分钟

YYKVStorage、YYKVStorageItem: YYKVStorage主要负责Sqlite储存封装,YYKVStorageItem保存了储存所需要的信息

YYCache源码分析

YYCache主要负责调度YYMemoryCache和YYDiskCache,来实现内存本地双通道存储功能,功能相对简单

更新和删除时,通过YYMemoryCache和YYDiskCache同时更新或者删除,读取指定信息时,先通过YYMemoryCache从内存中查找,找不到时通过YYDiskCache到磁盘查找,找到后并更新YYMemoryCache中的快速缓存

主要代码如下所示:

//通过key获取信息
- (void)objectForKey:(NSString *)key 
    withBlock:(void (^)(NSString *key, id<NSCoding> object))block {
    if (!block) return;
    //通过内容查找指定key的信息
    id<NSCoding> object = [_memoryCache objectForKey:key];
    if (object) {
        //找到直接block在子线程返回内容,这里我想是为了保持和本地磁盘找后均在子线程返回,避免出现隐形错误
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            block(key, object);
        });
    } else {
        //内存中没找到,到磁盘中查找,找到后并更新快速缓存
        [_diskCache objectForKey:key withBlock:^(NSString *key, id<NSCoding> object) {
            if (object && ![_memoryCache objectForKey:key]) {
                [_memoryCache setObject:object forKey:key];
            }
            block(key, object);
        }];
    }
}
//通过key更新内存和磁盘信息
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key];
}
//通过key删除内存和磁盘信息
- (void)removeObjectForKey:(NSString *)key {
    [_memoryCache removeObjectForKey:key];
    [_diskCache removeObjectForKey:key];
}

YYMemoryCache源码分析

YYMemoryCache是一个自动清理的内存缓存工具,其功能只负责快速缓存对象的处理,平时也可以完全替代NSCache使用,而不是使用YYCache框架就必须使用YYCache对象

其主要功能模块为:缓存功能模块、自动清理模块

双向链表_YYLinkedMap

_YYLinkedMap为一个双向链表,其节点为_YYLinkedMapNode,每一个_YYLinkedMapNode都有前驱和后继

结构如下所示:

@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

双向链表特点:增删比顺序表或者其他综合的哈希表都快,且在此缓存规则下,增删只需要一步,即更快

缓存功能模块

缓存功能是一个_YYLinkedMap双向链表和字典(CFMutableDictionaryRef)共同储存的一个模块,其中字典通过key-value的方式储存的是双向链表的一个个节点,这样可以保证读取时的快速,而_YYLinkedMap充分利用双向链表的快速增删特性,并且加入和删除操作总是在两端开始操作,因此链表增删操作只需要键值一步操作即可完成,因此大大提高了使用效率

注意:双向列表可以理解为一个特殊的队列

添加对象

添加对象过程需要同时维护好字典和双向链表的关系,且需要加锁来保证线程安全,及时使用者非常小心,也无法保证后台自动清理的安全性问题,因此这里必须加上互斥锁

互斥锁的使用采用了目前效率较高的pthread_mutex_lock锁,其效率和GCD信号量基本持平,且GCD信号量在等待时性能相对其较弱,平时略强,使用频率不高的话,两者实际用起来都差不多,加解锁频率高的可以优先使用pthread_mutex_lock

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    //加锁以保证操作安全
    pthread_mutex_lock(&_lock);
    //从字典中获取node节点,方便双向列表直接增删操作
    _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 {
        //当前节点不存在,创建并插入到队首,插入过程中,会set到字典当中,并且会增加总大小和总数量
        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在子线程中释放,减少主线程开销
                [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);
}

//插入对象的时候,除了快速将其放到队首,还增加了总数量和总大小
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), 
        (__bridge const void *)(node));
    _totalCost += node->_cost;
    _totalCount++;
    if (_head) {
        node->_next = _head;
        _head->_prev = node;
        _head = node;
    } else {
        _head = _tail = node;
    }
}

查找对象

查找对象功能相对简单,通过map查找,并且将刚找到的内容放到链表的最前面,以更新为最新访问

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

自动清理功能

自动清理功能调用方法为_trimRecursively,在该对象初始化时便延时调用,且注册了内存警告和切入后台操作,内存警告和切入后台,马上清理掉内容中的所有内容,以保证应用内存减少,增加应用后台存货率

这里会通过数量、大小、时间三个维度来清理缓存,其中默认都是最大值,即默认不清理,且由于逻辑相似这里只介绍一个处理方法

//延迟清理内存
- (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释放时,该方法不在调用,因此通过GCD的延时方法能减少功能的维护
        [self _trimInBackground];
        [self _trimRecursively]; //递归调用延时清理方法
    });
}
//子线程中通过数量、大小、时间三个维度来清理缓存,其中默认都是最大值,即默认不清理
- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
    });
}

//通过大小限制清理缓存(其他的两个都一样,这里就不多介绍了
- (void)_trimToCost:(NSUInteger)costLimit {
    BOOL finish = NO;
    pthread_mutex_lock(&_lock);
    //如果最大限制为0,则直接清理全部
    if (costLimit == 0) {
        [_lru removeAll];
        finish = YES;
    } else if (_lru->_totalCost <= costLimit) {
        //小于最大值才开始清理,否则不清理
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    NSMutableArray *holder = [NSMutableArray new];
    //根据总数量进行清理
    while (!finish) {
        //如果互斥锁已经锁定,则返回不为1的参数,当前已经加锁一次,还可以使用如果外部有方法调用,
        //则此时信号量低于0,尝试加锁会失败且返回不为0的数,因此此操作是为了避免外部在读写时删减
        if (pthread_mutex_trylock(&_lock) == 0) {
            //开始删除节点,并保存holder用于子线程释放
            if (_lru->_totalCost > costLimit) {
                _YYLinkedMapNode *node = [_lru removeTailNode];
                if (node) [holder addObject:node];
            } else {
                finish = YES;
            }
            //解除当前锁的占用
            pthread_mutex_unlock(&_lock);
        } else {
            //间隔10ms处理,避免死锁
            usleep(10 * 1000); //10 ms
        }
    }
    //未设置主线程,则在子线程中释放对象
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? 
            dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            [holder count]; // release in queue
        });
    }
}

YYDiskCache源码分析

YYDiskCache负责磁盘缓存的处理,包括调用YYKVStorage进行本地缓存的写入和读取,YYDiskCache主要负责缓存方法的读取功能以及根据缓存设置大小清理缓存功能,模式与YYMemoryCache很相似

_globalInstances

_globalInstances是保存多个YYDiskCache对象,用于区分不同路径下的对象保存,避免缓存出现问题

static NSMapTable *_globalInstances;
static dispatch_semaphore_t _globalInstancesLock;

//初始化globalInstances对象为NSMapTable类型,方便管理,当某个对象不使用时释放以减少内存
//另初始化信号量作为互斥锁使用
static void _YYDiskCacheInitGlobal() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _globalInstancesLock = dispatch_semaphore_create(1);
        _globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
    });
}

//获取根据path获取YYDiskCache对象
static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) {
    if (path.length == 0) return nil;
    _YYDiskCacheInitGlobal();
    dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
    id cache = [_globalInstances objectForKey:path];
    dispatch_semaphore_signal(_globalInstancesLock);
    return cache;
}
//初始化时根据key设置YYDiskCache对象
static void _YYDiskCacheSetGlobal(YYDiskCache *cache) {
    if (cache.path.length == 0) return;
    _YYDiskCacheInitGlobal();
    dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
    [_globalInstances setObject:cache forKey:cache.path];
    dispatch_semaphore_signal(_globalInstancesLock);
}

YYDiskCache初始化

YYDiskCache初始化的时候除了使用globalCache之外,还根据threshold区分了储存的类型,有三种:根据文件名以普通文件方式储存、保存到数据库、混合模式(小数据写入到数据库,大文件直接保存单个文件)

还初始化了YYKVStorage类,其主要是负责磁盘相关操作,包括数据库的读写磁盘操作以及普通文件的写入磁盘操作

与快速缓存一样,也开启了定时清理磁盘功能,间隔默认一分钟

- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    self = [super init];
    if (!self) return nil;
    
    YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if (globalCache) return globalCache;
    
    YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
    if (!kv) return nil;
    
    _kv = kv;
    _path = path;
    _lock = dispatch_semaphore_create(1);
    _queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
    _inlineThreshold = threshold;
    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _freeDiskSpaceLimit = 0;
    _autoTrimInterval = 60;
    
    [self _trimRecursively];
    _YYDiskCacheSetGlobal(self);
    
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(_appWillBeTerminated)
        name:UIApplicationWillTerminateNotification object:nil];
    return self;
}

读写缓存操作

写入操作会对对象先进行归档操作,然后根据类型判断怎么写入到文件中去

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    if (!key) return;
    //对象不存在直接删除该键值下的内容
    if (!object) {
        [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) {
        if (value.length > _inlineThreshold) {
            filename = [self _filenameForKey:key];
        }
    }
    
    //通过YYKVStorage写入到本地文件
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}

读取操作为直接到本地获取,获取的同时并更新本地文件时间信息

- (id<NSCoding>)objectForKey:(NSString *)key {
    if (!key) return nil;
    Lock();
    //读取本地的YYKVStorageItem类型,里面包含了Value的地址信息
    //如果有fileName则缓存数据Value,则单独保存文件到目录,否则就是归档数据保存在item的value中
    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;
}

自动清理操作

系统在初始化的时候就开启了自动清理工作,即调用一个递归的方法_trimRecursively,间隔一分钟进行一次清理

- (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];
    });
}
//主要清理方法,分别根据总大小、文件总数量、文件时间进行清理、磁盘空余时间,文件相关部分在YYKVStorage中介绍
- (void)_trimInBackground {
    __weak typeof(self) _self = self;
    dispatch_async(_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        Lock();
        [self _trimToCost:self.costLimit];
        [self _trimToCount:self.countLimit];
        [self _trimToAge:self.ageLimit];
        [self _trimToFreeDiskSpace:self.freeDiskSpaceLimit];
        Unlock();
    });
}
//根据磁盘剩余空间对缓存进行清理
- (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace {
    if (targetFreeDiskSpace == 0) return;
    //获取内容总大小
    int64_t totalBytes = [_kv getItemsSize];
    if (totalBytes <= 0) return;
    //根据磁盘实际总剩余和预设最大剩余空间来计算要清理的内容大小,然后通过设置上限进行清理
    int64_t diskFreeBytes = _YYDiskSpaceFree();
    if (diskFreeBytes < 0) return;
    int64_t needTrimBytes = targetFreeDiskSpace - diskFreeBytes;
    if (needTrimBytes <= 0) return;
    int64_t costLimit = totalBytes - needTrimBytes;
    if (costLimit < 0) costLimit = 0;
    //开始清理掉指定大小的数据
    [self _trimToCost:(int)costLimit];
}

YYKVStorage源码分析

YYKVStorage在YYDiskCache中被调用,其为对所有磁盘操作的一个文件封装,包括了数据库的初始化,表的创建、内容检索、内容删除、内容大小计算等

YYKVStorageItem

数据库缓存中保存的就是该类的信息,当有filename时,value值会被保存在相应的目录下,否则保存在value字段中

@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key;                ///< key
@property (nonatomic, strong) NSData *value;                ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size;    ///< value's size in bytes
@property (nonatomic) int modTime;     ///< modification unix timestamp
@property (nonatomic) int accessTime;       ///< last access unix timestamp
///< extended data (nil if no extended data)
@property (nullable, nonatomic, strong) NSData *extendedData;
@end

YYKVStorage

这里面主要介绍核心操作,关于数据库的细节操作,可以查看详细代码,照猫画虎相信可以自行设计出合适的内容

YYKVStorage的数据相关操作不是线程安全的,在YYDiskCache中对其进行了安全处理

YYKVStorage的初始化

其初始化过程中除了设置目录之外,还创建了数据库,设置了数据库表格table

- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type {
    if (path.length == 0 || path.length > kPathLengthMax) {
        NSLog(@"YYKVStorage init error: invalid path: [%@].", path);
        return nil;
    }
    if (type > YYKVStorageTypeMixed) {
        NSLog(@"YYKVStorage init error: invalid type: %lu.", (unsigned long)type);
        return nil;
    }
    
    self = [super init];
    _path = path.copy;
    _type = type;
    _dataPath = [path stringByAppendingPathComponent:kDataDirectoryName];
    _trashPath = [path stringByAppendingPathComponent:kTrashDirectoryName];
    _trashQueue = dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL);
    _dbPath = [path stringByAppendingPathComponent:kDBFileName];
    _errorLogsEnabled = YES;
    NSError *error = nil;
    if (![[NSFileManager defaultManager] createDirectoryAtPath:path
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:&error] ||
        ![[NSFileManager defaultManager] createDirectoryAtPath:
            [path stringByAppendingPathComponent:kDataDirectoryName]
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:&error] ||
        ![[NSFileManager defaultManager] createDirectoryAtPath:
            [path stringByAppendingPathComponent:kTrashDirectoryName]
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:&error]) {
        NSLog(@"YYKVStorage init error:%@", error);
        return nil;
    }
    
    //打开数据库并初始化数据库表格
    if (![self _dbOpen] || ![self _dbInitialize]) {
        // db file may broken...
        [self _dbClose];
        [self _reset]; // rebuild
        if (![self _dbOpen] || ![self _dbInitialize]) {
            [self _dbClose];
            NSLog(@"YYKVStorage init error: fail to open sqlite db.");
            return nil;
        }
    }
    [self _fileEmptyTrashInBackground]; // empty the trash if failed at last time
    return self;
}
//打开数据库操作
- (BOOL)_dbOpen {
    if (_db) return YES;
    
    int result = sqlite3_open(_dbPath.UTF8String, &_db);
    if (result == SQLITE_OK) {
        CFDictionaryKeyCallBacks keyCallbacks = kCFCopyStringDictionaryKeyCallBacks;
        CFDictionaryValueCallBacks valueCallbacks = {0};
        _dbStmtCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &keyCallbacks, &valueCallbacks);
        _dbLastOpenErrorTime = 0;
        _dbOpenErrorCount = 0;
        return YES;
    } else {
        _db = NULL;
        if (_dbStmtCache) CFRelease(_dbStmtCache);
        _dbStmtCache = NULL;
        _dbLastOpenErrorTime = CACurrentMediaTime();
        _dbOpenErrorCount++;
        
        if (_errorLogsEnabled) {
            NSLog(@"%s line:%d sqlite open failed (%d).", __FUNCTION__, __LINE__, result);
        }
        return NO;
    }
}
//初始化数据库表格
- (BOOL)_dbInitialize {
    NSString *sql = @"pragma journal_mode = wal; pragma synchronous = normal; create table if not exists manifest (key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key)); create index if not exists last_access_time_idx on manifest(last_access_time);";
    return [self _dbExecute:sql];
}

保存数据

保存数据是根据保存类型,将数据保存到数据库和单个文件当中

- (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;
    }
    //item中设置了保存的文件名字,则优先将value保存到单个文件中
    if (filename.length) {
        //将value保存到文件当中
        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];
    }
}

读取信息

读取信息除了在数据库中读取item的信息还需要读取到value数据,其可能存放在单个文件当中

- (NSData *)getItemValueForKey:(NSString *)key {
    if (key.length == 0) return nil;
    NSData *value = nil;
    switch (_type) {
        case YYKVStorageTypeFile: {
            //根据key从数据库中查询到其文件名,然后到相应的单个文件目录读取内容
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                //读取文件内容
                value = [self _fileReadWithName:filename];
                if (!value) {
                    //没内容就删除文件
                    [self _dbDeleteItemWithKey:key];
                    value = nil;
                }
            }
        } break;
        case YYKVStorageTypeSQLite: {
            //仅仅存放在数据库中,则直接读取数据库中的value即可
            value = [self _dbGetValueWithKey:key];
        } break;
        case YYKVStorageTypeMixed: {
            //混合状态,存在文件名时,和读取文件一样先到数据库中查路径,然后到单个文件读取内容
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                value = [self _fileReadWithName:filename];
                if (!value) {
                    [self _dbDeleteItemWithKey:key];
                    value = nil;
                }
            } else {
                //如果没有文件名,则直接在数据库中的value中取出相应信息
                value = [self _dbGetValueWithKey:key];
            }
        } break;
    }
    if (value) {
        [self _dbUpdateAccessTimeWithKey:key];
    }
    return value;
}

删除数据

删除数据是根据类型该清理数据库的清理数据库,该删除文件的删除文件

- (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];
            }
            //数据库中删除该item信息
            return [self _dbDeleteItemWithKey:key];
        } break;
        default: return NO;
    }
}

根据内容大小、数量、时间

其逻辑大同小异,这里只介绍大小作为案例

- (BOOL)removeItemsToFitSize:(int)maxSize {
    //没设置清理限制时,则默认不清理,否则达到最大值是,则开始清理
    if (maxSize == INT_MAX) return YES;
    if (maxSize <= 0) return [self removeAllItems];
    
    int total = [self _dbGetTotalItemSize];
    if (total < 0) return NO;
    if (total <= maxSize) return YES;
    
    NSArray *items = nil;
    BOOL suc = NO;
    //每次取出16条信息进行删除,并更新总数量,如果总数量没超过限制或者空了,则直接结束
    do {
        int perCount = 16;
        items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
        for (YYKVStorageItem *item in items) {
            if (total > maxSize) {
                if (item.filename) {
                    [self _fileDeleteWithName:item.filename];
                }
                suc = [self _dbDeleteItemWithKey:item.key];
                total -= item.size;
            } else {
                break;
            }
            if (!suc) break;
        }
    } while (total > maxSize && items.count > 0 && suc);
    if (suc) [self _dbCheckpoint];
    return suc;
}

最后

YYCache的框架逻辑清晰,每个模块相对比较独立,均可以支撑起相应功能,我们可以在设置自己框架的时候参考其思想,来优化自己项目质量

另外YYKit里面还有很多我们要学习的东西,一起来探索提升吧