YYCache 源码解析

YYCache 源码解析

项目地址:YYCache,分析的版本:4e78fa8

1. 功能介绍

YYCache 是 ibireme 大神设计的一款缓存库。阅读本文之前推荐大家先看下原作者自己的设计思路文章

2. 总体设计

2.1 总体设计图

总体设计请参考 4.1 类关系图

2.2 YYCache 中的概念

简单介绍下一些相关名词。
Cost:理解为成本和开销都可以,比如,在内存缓存中对应的概念就是内存的开销,但并不是指所缓存对象的大小,这个开销的大小在调用方法的时候传入。

totalCost:总的开销。

Count:缓存对象的个数

totalCount:缓存对象的总个数。

age:缓存的过期时间。如果一个缓存对象时间减去当前时间的值大于 age,那么这个对象就需要被移除。

2.3 阅读Tips

YYCache 代码量十分少,去掉注释的话不到2k行,十分推荐大家去阅读一次源码。

下面简单说下 YYCache 的整体设计,缓存一般就是分为内存缓存和磁盘缓存,流程步骤可以查看下面的流程图。

淘汰算法方面的话是使用 LRU 淘汰算法,全称就是 Least Recently Used 近期最少使用,顾名思义就是把最少使用的数据淘汰。

内存缓存这边使用的数据结构则是双向链表,然后为了优化查找这方面的时间就增加了一个 NSDictionary 来保存数据关系,这样就做到了增删查改的时间复杂度为O(1)。

磁盘缓存这边分为数据库存储和文件存储两种方式,这是因为单条数据大于20K的时候直接写为文件速度会更快一些,低于的20K的时候 SQLite 读取性能会更好。

每个数据都会记录到数据库当中,数据表里面会有一个叫 filename 的字段,表明文件名字,所以如果有个数据是用文件形式存储的话,也会在表中有一条对应的数据,在使用一次之后会更新它的上次访问时间。代码当中是根据传进来的 filename 是否为空来判断到底是存数据库还是存文件。

3. 流程图

整体流程图

st=>start: 查找
e=>end: 结束
diskOp=>condition: 磁盘缓存中查找
cond=>condition: 内存缓存中查找
set2Memory=>operation: 将数据添加到内存缓存当中

st->cond
diskOp(yes)->set2Memory->e
diskOp(no)->e
cond(yes)->e
cond(no)->diskOp

磁盘缓存保存数据流程图

st=>start: 传入数据(key,value,filename)
e=>end: 结束
diskOp=>operation: 保存数据至文件
diskOp2=>operation: 保存记录至数据库
cond=>condition: filename 是否为空
typeCond=>operation: 保存数据至数据库
set2Memory=>operation: 将数据添加到内存缓存当中


st->cond
diskOp(yes)->set2Memory->e
diskOp(no)->e
cond(yes)->typeCond->e
cond(no)->diskOp->diskOp2->e

4. 详细设计

4.1 类关系图

4.2 项目结构


通过上图可以知道其实就分两块,内存缓存和磁盘缓存。

4.3 详细介绍

4.3.1 YYCache

YYCache 持有 YYDiskCache 和 YYMemoryCache 的实例

从上图来看,代码分为两块。
属性的话就三个属性,用于存储名字的 name,以及内存缓存的 memoryCache 和磁盘缓存 diskCache
然后提供了创建实例的 init 相关方法,以及使用 UNAVAILABLE_ATTRIBUTE 来禁用 init 和 new 方法。

然后下面的一堆方法就是对外提供了 CRUD 的功能,并且每个功能都提供了异步回调的形式。具体实现的话基本都是差不多,就是调用 diskCache 和 memoryCache 的相关方法。比如下面的这个方法。

- (BOOL)containsObjectForKey:(NSString *)key {
    return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}

- (void)containsObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key, BOOL contains))block {
    if (!block) return;

    if ([_memoryCache containsObjectForKey:key]) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            block(key, YES);
        });
    } else  {
        [_diskCache containsObjectForKey:key withBlock:block];
    }
}
4.3.2 YYMemoryCache


上图可以看出,分成四块,基本看名字就能明白作用了。

YYMemoryCache 使用双向链表和 NSDictionary 来实现 LRU 算法。

这里我默认大家都是懂双向链表的,所以链表相关的我会略过,但是我做了个小动画演示,相信就算不懂的人看完应该也就懂了。

比如当前链表的节点就三个数据,从头到尾分别是小明,小白,小黑,如果这个时候外部获取了一次小黑,这个时候就需要更新小黑这个节点的访问时间,并把小黑移到头部。

对应的功能代码如下

- (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;
    }
    node->_next = _head;
    node->_prev = nil;
    _head->_prev = node;
    _head = node;
}

YYMemoryCache 私有方法

//循环剪枝,调用这个方法后就会每隔一段时间在后台确认下有没需要剔除的数据。

- (void)_trimRecursively

//后台剪枝,循环剪枝的主要调用方法。会异步的根据Cost、Count、Age去剪枝

- (void)_trimInBackground

//剪枝到某个大小,如果是0的话就是全部清空,如果当前链表的Cost小于目标值,那就不用剪枝了直接返回。
//如果需要剪枝的话,也就是链表的Cost大于目标值,那么就把链表尾部的Node拿出来,并放到一个Array里,然后不停的比较,直到链表的Cost小于目标值。
//关于上面那个array为何要在block里面调用count方法,是因为可以根据配置在不同线程中进行释放,之所以调用只是保证编译器不会优化掉这个操作

- (void)_trimToCost:(NSUInteger)costLimit

//剪枝到某个个数,如果是0的话就是全部清空,如果当前链表的Count小于目标值,那就不用剪枝了直接返回。
//如果需要剪枝的话,也就是链表的Count大于目标值,那么久把链表尾部的Node拿出来,并放到一个Array里,然后不停的比较,直到链表的Cost小于目标值。
//关于上面那个array为何要在block里面调用count方法,是因为可以根据配置在不同线程中进行释放,之所以调用只是保证编译器不会优化掉这个操作

- (void)_trimToCount:(NSUInteger)countLimit

//同上。只是对过期时间的判断,比如过期时间限制是3600秒,看Node的Time距离现在为止过了多久,是否小于3600秒。

- (void)_trimToAge:(NSTimeInterval)ageLimit

// 内存警告的通知,如果对应的block非nil,就调用

- (void)_appDidReceiveMemoryWarningNotification
// 切换到后台的通知,如果对应的block非nil,就调用

- (void)_appDidEnterBackgroundNotification

YYMemoryCache 公有方法

//初始化相关参数,注册监听通知,开启后台剪枝

- (instancetype)init

//销毁相关参数

- (void)dealloc

//下面这些方法最终都是在加锁的状态下调用 _YYLinkedMap 相关的方法

- (NSUInteger)totalCount
- (NSUInteger)totalCost
- (BOOL)releaseOnMainThread
- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread
- (BOOL)releaseAsynchronously
- (void)setReleaseAsynchronously:(BOOL)releaseAsynchronously
- (BOOL)containsObjectForKey:(id)key

//通过key获取value,实际上是从 _YYLinkedMap 的NSDictionary 里面取值,取完值之后还要把 对应 Node 的Time 更新为当前时间,并把 Node 移到链表头部。

- (id)objectForKey:(id)key

- (void)setObject:(id)object forKey:(id)key

//根据 key,cost 设置 object
//如果 object 为 nil就移除对应的 key。
//如果存在 Node,就更新 Cost,Time,Value,并把 Node 移到链表头部。
//如果不存在 Node,就实例一个 Node,并设置对应值,然后添加到链表头部。
//然后检查 _YYLinkedMap 的 Cost 和 Count,如果超出就调用剪枝方法,并且会根据参数来决定是否在指定的线程释放资源。

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost

// 调用 _YYLinkedMap 的删除方法,并根据参数来决定是否在指定的线程释放资源。

- (void)removeObjectForKey:(id)key
//直接调用 _YYLinkedMap 的删除方法

- (void)removeAllObjects

//内部就是调用对应的私有方法
- (void)trimToCount:(NSUInteger)count
- (void)trimToCost:(NSUInteger)cost
- (void)trimToAge:(NSTimeInterval)age

//略
- (NSString *)description

_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

@implementation _YYLinkedMapNode
@end

_YYLinkedMap,链表

@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic;
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head;
    _YYLinkedMapNode *_tail; 
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

- (void)removeNode:(_YYLinkedMapNode *)node;

- (_YYLinkedMapNode *)removeTailNode;

- (void)removeAll;

@end

上面具体代码就不详细分析了,都是十分简单的代码,就是双向链表的数据结构,然后在做增删改查的时候对 Time、Count、Cost 这些属性进行修改,还有就是维护一个 NSDictionary,链表的 Node 也会被这个 NSDictionary 持有,这样就做到了作者所说的,增、删、改、查、清空的时间复杂度都是 O(1)。

4.3.3 YYDiskCache

YYDiskCache 也是使用 LRU 算法,也是由 cost、count 和 age 来进行控制,可持续化存储采用了 sqlite 和 file 两种存储的形式,具体原因 ibireme 也在原文当中做了解释。
其实 YYDiskCache 主要是在调用 YYKVStorage 实例的方法,算是对它的 wrap。

当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些

下面简单的说下头文件里面的内容

//-----------属性讲解
//缓存文件名字
@property (nullable, copy) NSString *name;
//缓存文件路径
@property (readonly) NSString *path;
//是否使用 file 进行可持续化存储的临界值,如果数据大小大于这个临界值,就会使用 SQLite
@property (readonly) NSUInteger inlineThreshold;
//如果实现了这个 Block,就会代替 NSKeyedArchiver 的归档
@property (nullable, copy) NSData *(^customArchiveBlock)(id object);
//如果实现了这个 Block,就会代替 NSKeyedArchiver 的解档
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data);
//如果实现了这个 Block,对象被存储为文件的时候,名字由此返回。
@property (nullable, copy) NSString *(^customFileNameBlock)(NSString *key);

//-----------剪枝的限制    
@property NSUInteger countLimit;
@property NSUInteger costLimit;
@property NSTimeInterval ageLimit;
//剩余空间限制,如果磁盘剩余空间低于这个值,就会去移除对象
@property NSUInteger freeDiskSpaceLimit;
//自动剪枝的时间
@property NSTimeInterval autoTrimInterval;
//日志开启的状态值
@property BOOL errorLogsEnabled;


//根据路径初始化
- (nullable instancetype)initWithPath:(NSString *)path;
//根据路径和临界值进行初始化
- (nullable instancetype)initWithPath:(NSString *)path
                      inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER;

//-----------访问方法,就是CRUD
- (BOOL)containsObjectForKey:(NSString *)key;
- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block;
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id<NSCoding> _Nullable object))block;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(void(^)(void))block;
- (void)removeObjectForKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key))block;
- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
                                 endBlock:(nullable void(^)(BOOL error))end;
- (NSInteger)totalCount;
- (void)totalCountWithBlock:(void(^)(NSInteger totalCount))block;
- (NSInteger)totalCost;
- (void)totalCostWithBlock:(void(^)(NSInteger totalCost))block;


//-----------剪枝方法
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCount:(NSUInteger)count withBlock:(void(^)(void))block;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToCost:(NSUInteger)cost withBlock:(void(^)(void))block;
- (void)trimToAge:(NSTimeInterval)age;
- (void)trimToAge:(NSTimeInterval)age withBlock:(void(^)(void))block;

下面说下实现文件里面的内容

//查询当前剩余空间
static int64_t _YYDiskSpaceFree(){}
//MD5加密
static NSString *_YYNSStringMD5(NSString *string) {}

static NSMapTable *_globalInstances;
static dispatch_semaphore_t _globalInstancesLock;

//对 _globalInstances 和 _globalInstancesLock 的初始化,这里使用了 NSMapTable 来作为容器,而且 valueOptions 是 weak
static void _YYDiskCacheInitGlobal() {}
// 使用 YYDiskCache 的 path 作为 key,自身作为 value
static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) {}
static void _YYDiskCacheSetGlobal(YYDiskCache *cache) {}

//这里的实现跟 YYMemoryCache 基本一致
- (void)_trimRecursively{}
- (void)_trimInBackground{}

//下面这两个方法实际上是调用 YYKVStorage 的相关方法
- (void)_trimToCost:(NSUInteger)costLimit {}
- (void)_trimToCount:(NSUInteger)countLimit {}

// 这个方法我觉得有些奇怪,下面的 removeItemsEarlierThanTime 传入的参数明显应该是时间戳
// 但是计算出来的 age 的值又应该是当前时间和目标时间的差值。
// 所以我觉得这个方法应该是写错了,如果有明白的朋友可以发表下看法,共同交流下:)
- (void)_trimToAge:(NSTimeInterval)ageLimit {
    if (ageLimit <= 0) {
        [_kv removeAllItems];
        return;
    }
    long timestamp = time(NULL);
    if (timestamp <= ageLimit) return;
    long age = timestamp - ageLimit;
    if (age >= INT_MAX) return;
    [_kv removeItemsEarlierThanTime:(int)age];
}

//控制磁盘剩余空间容量
//先是获取当前缓存的总大小,再获取磁盘剩余空间大小,
//将目标值 - 剩余空间大小,得出需要剪枝的大小
//然后将当前缓存的总大小 - 需要剪枝的大小,得出的就是最终的目标大小。
- (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace {}
//如果有自定义Block就用自定义的,否则用MD5
- (NSString *)_filenameForKey:(NSString *)key{}


- (instancetype)initWithPath:(NSString *)path{}
// 会根据临界值来决定存储的类型是 File 类型 还是 SQLite类型 还是 混合类型
// 然后初始化属性,并将自己设置到全局MapTable当中去
- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold{}


//访问方法,没啥好讲的
- (BOOL)containsObjectForKey:(NSString *)key
- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block
- (id<NSCoding>)objectForKey:(NSString *)key
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id<NSCoding> object))block
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key withBlock:(void(^)(void))block
- (void)removeObjectForKey:(NSString *)key
- (void)removeObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key))block
- (void)removeAllObjects
- (void)removeAllObjectsWithBlock:(void(^)(void))block
- (void)removeAllObjectsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress
                                 endBlock:(void(^)(BOOL error))end
- (NSInteger)totalCount
- (void)totalCountWithBlock:(void(^)(NSInteger totalCount))block
- (NSInteger)totalCost
- (void)totalCostWithBlock:(void(^)(NSInteger totalCost))block
- (void)trimToCount:(NSUInteger)count
- (void)trimToCount:(NSUInteger)count withBlock:(void(^)(void))block
- (void)trimToCost:(NSUInteger)cost
- (void)trimToCost:(NSUInteger)cost withBlock:(void(^)(void))block
- (void)trimToAge:(NSTimeInterval)age
- (void)trimToAge:(NSTimeInterval)age withBlock:(void(^)(void))block

//拓展数据的存取
+ (NSData *)getExtendedDataFromObject:(id)object
+ (void)setExtendedData:(NSData *)extendedData toObject:(id)object
4.3.4 YYKVStorage

先看下 YYKVStorageItem,都是些见名知其意的命名,就不用赘述了。

@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
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end

然后再看下 YYKVStorageType 的定义,表示着三种存储模式,纯文件存储、纯SQLite存储和两者结合的混合存储。

typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    YYKVStorageTypeFile = 0,
    YYKVStorageTypeSQLite = 1,
    YYKVStorageTypeMixed = 2,
};

接下来涉及的 SQLite 相关知识对于我来说比较薄弱,就会仔细讲解下代码。

首先看下定义的几个 static 常量。

//错误尝试次数最大值
static const NSUInteger kMaxErrorRetryCount = 8;
//尝试间隔最小值
static const NSTimeInterval kMinRetryTimeInterval = 2.0;
//路径最大长度
static const int kPathLengthMax = PATH_MAX - 64;
//数据库名字
static NSString *const kDBFileName = @"manifest.sqlite";
//数据库的 shm 文件
static NSString *const kDBShmFileName = @"manifest.sqlite-shm";
//数据库的 wal 文件
static NSString *const kDBWalFileName = @"manifest.sqlite-wal";
//数据存放文件夹的名字
static NSString *const kDataDirectoryName = @"data";
//被删文件存放的文件夹的名字
static NSString *const kTrashDirectoryName = @"trash";

我对其他常量看名字就能知道作用,但是对于 shm 和 wal 这两个文件就一头雾水,然后谷歌一番才明白 WAL(Write Ahead Logging) 是数据库实现原子事务的一种机制。

WAL机制的原理是:修改并不直接写入到数据库文件中,而是写入到另外一个称为WAL的文件中;如果事务失败,WAL中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改。
引用至SQLite的WAL机制

接下来看下文件目录结构和数据库表结构,从下代码可以知道,使用文件存储的数据会存放到 data 目录下,被删除的的数据则会在 trash 目录下。

/*
 File:
 /path/
      /manifest.sqlite
      /manifest.sqlite-shm
      /manifest.sqlite-wal
      /data/
           /e10adc3949ba59abbe56e057f20f883e
           /e10adc3949ba59abbe56e057f20f883e
      /trash/
            /unused_file_or_folder

 SQL:
 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)
 ); 
 */

下面来看下初始化相关的代码

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

基本上就是初始化属性,然后打开数据库,再接着就是清空 Trash 里面的文件。然后我们接下看下 db 相关的几个方法,先看下 open 和 close 方法。

@implementation YYKVStorage {
    dispatch_queue_t _trashQueue;

    NSString *_path;
    NSString *_dbPath;
    NSString *_dataPath;
    NSString *_trashPath;

    sqlite3 *_db;
    CFMutableDictionaryRef _dbStmtCache;
    NSTimeInterval _dbLastOpenErrorTime;
    NSUInteger _dbOpenErrorCount;
}

- (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)_dbClose {
    if (!_db) return YES;

    int  result = 0;
    BOOL retry = NO;
    BOOL stmtFinalized = NO;

    if (_dbStmtCache) CFRelease(_dbStmtCache);
    _dbStmtCache = NULL;

    do {
        retry = NO;
        result = sqlite3_close(_db);
        if (result == SQLITE_BUSY || result == SQLITE_LOCKED) {
            if (!stmtFinalized) {
                stmtFinalized = YES;
                sqlite3_stmt *stmt;
                while ((stmt = sqlite3_next_stmt(_db, nil)) != 0) {
                    sqlite3_finalize(stmt);
                    retry = YES;
                }
            }
        } else if (result != SQLITE_OK) {
            if (_errorLogsEnabled) {
                NSLog(@"%s line:%d sqlite close failed (%d).", __FUNCTION__, __LINE__, result);
            }
        }
    } while (retry);
    _db = NULL;
    return YES;
}

上面的代码也十分清晰明了,就是对 sqlite 的连接和关闭,以及对失败的计数,唯一需要讲的就是 statement cache,这里使用 CFDictionary 作为 statement cache,讲的土一点就是 sql 语句缓存。

数据库使用statement本身作为key并将存取方案存入与statement对应的缓存中。这样数据库引擎就可以对曾经执行过的statements中的存取方案进行重用。举个例子,如果我们发送一条包含SELECT a, b FROM t WHERE c = 2的statement到数据库,然后首先会将存取方案进行缓存。当我们再次发送相同的statement时,数据库会对先前使用过的存取方案进行重用,这样就降低了CPU的开销。
引用至JDBC

// 数据库初始化语句执行
- (BOOL)_dbInitialize {}
// 事务执行成功后,将 wal 合并到 db 当中
- (void)_dbCheckpoint {}


- (BOOL)_dbExecute:(NSString *)sql {
    if (sql.length == 0) return NO;
    if (![self _dbCheck]) return NO;

    char *error = NULL;
    int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &error);
    if (error) {
        if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite exec error (%d): %s", __FUNCTION__, __LINE__, result, error);
        sqlite3_free(error);
    }

    return result == SQLITE_OK;
}

- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql {
    if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL;
    sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql));
    if (!stmt) {
        int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
        if (result != SQLITE_OK) {
            if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
            return NULL;
        }
        CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt);
    } else {
        sqlite3_reset(stmt);
    }
    return stmt;
}

_dbExecute 方法的话就是常规的执行 sql 语句。
_dbPrepareStmt 的话则是获取 PrepareStatement,如果有 PrepareStmt 的缓存就直接 reset 并返回 PrepareStmt,如果没的话就生成 PrepareStmt,并将 sql 语句作为 key,PrepareStmt 作为value 存到 _dbPrepareStmt

//略
- (NSString *)_dbJoinedKeys:(NSArray *)keys
- (void)_dbBindJoinedKeys:(NSArray *)keys stmt:(sqlite3_stmt *)stmt fromIndex:(int)index
// 做的插入或更新的活。拿到 stmt 然后进行数值绑定,再执行 stmt
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
//上面略
//从这里可以看出,是根据传进来的 fileName 来决定是否存到 SQLite 里面
 if (fileName.length == 0) {
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
    } else {
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
//下面略  
}
// 通过 key 来更新访问时间
- (BOOL)_dbUpdateAccessTimeWithKey:(NSString *)key
- (BOOL)_dbUpdateAccessTimeWithKeys:(NSArray *)keys
// 通过 keys 来删除对应记录
- (BOOL)_dbDeleteItemWithKey:(NSString *)key
- (BOOL)_dbDeleteItemWithKeys:(NSArray *)keys
// 删除大于 size 的记录,这边的删除还只是数据库的删除
- (BOOL)_dbDeleteItemsWithSizeLargerThan:(int)size
// 删除时间早于 time 的记录
- (BOOL)_dbDeleteItemsWithTimeEarlierThan:(int)time
// 传入 Stmt 返回 YYKVStorageItem
- (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData {
    int i = 0;
    char *key = (char *)sqlite3_column_text(stmt, i++);
    char *filename = (char *)sqlite3_column_text(stmt, i++);
    int size = sqlite3_column_int(stmt, i++);
    const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i);
    int inline_data_bytes = excludeInlineData ? 0 : sqlite3_column_bytes(stmt, i++);
    int modification_time = sqlite3_column_int(stmt, i++);
    int last_access_time = sqlite3_column_int(stmt, i++);
    const void *extended_data = sqlite3_column_blob(stmt, i);
    int extended_data_bytes = sqlite3_column_bytes(stmt, i++);

    YYKVStorageItem *item = [YYKVStorageItem new];
    if (key) item.key = [NSString stringWithUTF8String:key];
    if (filename && *filename != 0) item.filename = [NSString stringWithUTF8String:filename];
    item.size = size;
    if (inline_data_bytes > 0 && inline_data) item.value = [NSData dataWithBytes:inline_data length:inline_data_bytes];
    item.modTime = modification_time;
    item.accessTime = last_access_time;
    if (extended_data_bytes > 0 && extended_data) item.extendedData = [NSData dataWithBytes:extended_data length:extended_data_bytes];
    return item;
}
//根据 key 来获取 YYKVStorageItem ,其实流程就是先拿到 Stmt 查到对应记录,再调用上面的方法来获得 YYKVStorageItem
- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData{}
// 同上类似
- (NSMutableArray *)_dbGetItemWithKeys:(NSArray *)keys excludeInlineData:(BOOL)excludeInlineData{}
//根据 key 获取 inline_data
- (NSData *)_dbGetValueWithKey:(NSString *)key{}
//根据 key 获取 filename
- (NSString *)_dbGetFilenameWithKey:(NSString *)key{}
- (NSMutableArray *)_dbGetFilenameWithKeys:(NSArray *)keys {}
// 获取大小大于指定值的所有名字,这个方法被用于删除大小大于指定值的所有文件
- (NSMutableArray *)_dbGetFilenamesWithSizeLargerThan:(int)size {}

// 获取早于指定时间的所有名字,这个方法被用于删除时间小于指定值的所有文件
- (NSMutableArray *)_dbGetFilenamesWithTimeEarlierThan:(int)time {}
//【最后访问时间】升序排列返回前 count 个 YYKVStorageItem,其实就是拿到最少使用的那几个数据。
- (NSMutableArray *)_dbGetItemSizeInfoOrderByTimeAscWithLimit:(int)count {}
// 通过 key 获取记录数,其实就是拿来判断是否存在这个 key
- (int)_dbGetItemCountWithKey:(NSString *)key {}
// 获取文件总大小
- (int)_dbGetTotalItemSize {}
// 获取总记录数
- (int)_dbGetTotalItemCount {}

//下面文件操作的根目录都是 data 目录
// 写数据至文件
- (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data {}
// 根据文件名读数据
- (NSData *)_fileReadWithName:(NSString *)filename {}
//根据文件名删除文件
- (BOOL)_fileDeleteWithName:(NSString *)filename {}
//把文件移到垃圾桶
- (BOOL)_fileMoveAllToTrash {}
// 把垃圾桶里面的文件都删除
- (void)_fileEmptyTrashInBackground {}

上面就把 DB 和 File 相关的方法都讲完了,接下来说下其他的方法。

- (BOOL)saveItem:(YYKVStorageItem *)item {}
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value {}
- (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;
    }

    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 {
        if (_type != YYKVStorageTypeSQLite) {//如果没有给文件名字,并且不是 SQLite 模式,就把对应的文件删除先,然后再数据保存到数据库。
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}
// 如果是 SQLite 模式就删除数据库记录就是了,如果是其他模式,先删文件,再删数据库记录
- (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;
    }
}
//同上类似
- (BOOL)removeItemForKeys:(NSArray *)keys {}
//删除大于指定大小的文件,数据库在每次使用 stmt 操作之后都需要调用一次 _dbCheckpoint 方法
//文件删除的话就直接删除就行了,下面的其他方法基本类似
- (BOOL)removeItemsLargerThanSize:(int)size {
    if (size == INT_MAX) return YES;
    if (size <= 0) return [self removeAllItems];

    switch (_type) {
        case YYKVStorageTypeSQLite: {
            if ([self _dbDeleteItemsWithSizeLargerThan:size]) {
                [self _dbCheckpoint];
                return YES;
            }
        } break;
        case YYKVStorageTypeFile:
        case YYKVStorageTypeMixed: {
            NSArray *filenames = [self _dbGetFilenamesWithSizeLargerThan:size];
            for (NSString *name in filenames) {
                [self _fileDeleteWithName:name];
            }
            if ([self _dbDeleteItemsWithSizeLargerThan:size]) {
                [self _dbCheckpoint];
                return YES;
            }
        } break;
    }
    return NO;
}
- (BOOL)removeItemsEarlierThanTime:(int)time {}
- (BOOL)removeItemsToFitSize:(int)maxSize {}
- (BOOL)removeItemsToFitCount:(int)maxCount {}
- (BOOL)removeAllItems {}
- (void)removeAllItemsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress
                               endBlock:(void(^)(BOOL error))end {}

//根据 key 获得 YYKVStorageItem,拿到 item 之后先更新数据库里面的上次访问时间,如果文件名不为空,就去读对应路径文件的数据,再赋值给 value。
- (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) {
            item.value = [self _fileReadWithName:item.filename];
            if (!item.value) {
                [self _dbDeleteItemWithKey:key];
                item = nil;
            }
        }
    }
    return item;
}
//根据 key 获得 YYKVStorageItem,但是这个方法获取的是没有拿到 value,因为传入的 excludeInlineData 是 YES,这是属于获取比较轻量的数据信息。
- (YYKVStorageItem *)getItemInfoForKey:(NSString *)key {
    if (key.length == 0) return nil;
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:YES];
    return item;
}

//根据 key 获取值,如果 type 是 file的话就读取文件,如果是 SQLite 的话就读取数据库
//如果是混合模式,就先读下数据库里面的 filename,存在这个文件就读取文件,不然就读数据库里面的数据
- (NSData *)getItemValueForKey:(NSString *)key {
    if (key.length == 0) return nil;
    NSData *value = nil;
    switch (_type) {
        case YYKVStorageTypeFile: {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                value = [self _fileReadWithName:filename];
                if (!value) {
                    [self _dbDeleteItemWithKey:key];
                    value = nil;
                }
            }
        } break;
        case YYKVStorageTypeSQLite: {
            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 = [self _dbGetValueWithKey:key];
            }
        } break;
    }
    if (value) {
        [self _dbUpdateAccessTimeWithKey:key];
    }
    return value;
}
- (NSArray *)getItemForKeys:(NSArray *)keys {}
//同 getItemInfoForKey
- (NSArray *)getItemInfoForKeys:(NSArray *)keys {}
- (NSDictionary *)getItemValueForKeys:(NSArray *)keys {}
// 是否存在 key 的记录
- (BOOL)itemExistsForKey:(NSString *)key {}
//获取缓存文件总个数
- (int)getItemsCount {}
//获取缓存文件总大小
- (int)getItemsSize {}

5. 杂谈

YYCache 整个项目算的上是短小精干,不足2k行的代码内容却是很充实,很适合作为初读的开源项目。

我在阅读内存缓存方面倒是没太花时间,因为这种 LRU 的设计跟 Android 的图片加载库 UIL 设计的是类似的,UIL 使用的是 LruMemoryCache,也是使用的链表结构的 LinkedHashMap,有兴趣的朋友也可以去看看这方面的代码。

画时间较多的则是在磁盘缓存方面,尤其是 filename 那边一开始没看懂,最后思考了蛮久才明白是用这个来做区分文件模式和 SQLite 模式。还有就是数据库的 wal 机制这方面也是了解不深,算是补了一下这方面的知识。

参考文档

  1. JDBC
  2. SQLite的WAL机制
2016-12-22 21:4447