# YYCache深入学习

## 简介

YYCahce 是YYkit组件库中的一部分，YYCache提供了内存缓存，和持久性的硬盘缓存。

## 一个合理缓存应该有的设计

* 合理的增删改查接口
* 高速缓存，提高常用缓存的返回性能和效率
* 低速缓存，磁盘大文件缓存
* 良好缓存限制策略
* 高性能，线程安全

## 基本设计思路

YYCache 提供对外的整合接口，YYMemoryCache 提供内存存储缓存，通过lru算法进行处理，YYDiskCache提供file和sqlite3的两种持久化存储方式

## YYMemoryCache

YYMemoryCahce 作为一个内存缓存，提供高速缓存，并且因为内存有限，需要进行一定的限制

1.线程安全

在频率高的并发数据操作中，必须保证线程安全，不然拿到的数据不符合预期，或者导致读写冲突 在YYMemoryCache中，采用的pthread\_mutex加锁

pthread\_mutex 定义了一组跨平台的线程相关的 API，pthread\_mutex 表示互斥锁。互斥锁的实现原理与信号量非常相似，不是使用忙等，而是阻塞线程并睡眠，需要进行上下文切换。

```
// 主要使用api
pthread_mutex_lock(&mutex); // 申请锁
// 临界区
pthread_mutex_unlock(&mutex); // 释放锁
```

pthread\_mutex支持递归锁，递归锁，就是在内部加锁调用的时候，递归了自身，这样子，会导致死锁。

* pthread\_mutex则支持递归处理 attr PTHREAD\_MUTEX\_RECURSIVE
* memory中使用pthread，在循环释放的时候，通过pthread\_mutex\_trylock实现简单的自旋锁

原文：OSSpinLock 和 dispatch\_semaphore 都不会产生特别明显的死锁，所以我也无法确定用 dispatch\_semaphore 代替 OSSpinLock 是否正确。能够肯定的是，用 pthread\_mutex 是安全的。

2.LRU

最近使用优先，也就是认为，最近使用的，最大可能性会再次用到 里面实现用到一个双向链表的结构进行处理，使用到的就会被移动到表头 在删除释放的时候，就会从队尾进行删除释放

3.键值对存储操作 通过创建链表节点node，进行间接操作数据

```
#pragma mark - _YYLinkedMapNode
@interface _YYLinkedMapNode : NSObject {
// 类似C中的private_extern，使用@private的话，限制太大，@package在类的镜像外进行引用就会报错
// 使用@public @protect等的话，就没什么限制的
// 目的是，限制在本文件中使用
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // 通过dic进行持有
__unsafe_unretained _YYLinkedMapNode *_next; // 通过dic进行持有
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
```

4.限制策略

```
// 限制条件有3个，age cost count ，一般情况下默认都不做限制
/// 个数限制
@property NSUInteger countLimit;
/// 过期时间
@property NSTimeInterval ageLimit;
/// 存储消耗限制，setObject的时候时候把内存大小的存进去
@property NSUInteger costLimit;

@property BOOL shouldRemoveAllObjectsWhenEnteringBackground; // 进入后台检查限制删除缓存，默认YES
@property BOOL shouldRemoveAllObjectsOnMemoryWarning; // 收到内存警告的时候
```

MemoryCache中使用的LRU的的算法进行删除缓存，当超过一定的限制的时候，会进行循环清理，也就是说，memory里面的东西并不是存进去就会在app的生命周期中一直存在的，可能会被释放掉。因此外部在使用的时候，需要判空处理的，如果为nil，则认为缓存已经失效，需要重新更新。

因此，YYMemoryCahce并不是数据在app的生命周期会被一直保留的，所以，在使用YYCahce最外层接口的时候，YYCache是会通过YYMemoryCache提供高速缓存，同时存入到低速缓存中。

获取的时候，YYMemoryCache获取不到，则会询问YYDiskCache。

5.某些小技巧，用于提供性能 会在一段时间，自行检查是否超过限制策略，超过则循环释放。 YYMemoryCache默认在进入后台的时候会进行检查。 因为一次释放太多，会导致资源消耗过大，因此，通过dispatch\_aync block的方式，放到后台线程释放

### YYDiskCache

1.线程安全 disk中使用dispatch\_semaphore信号量进行处理

dispatch\_semaphore 是信号量，但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时，它的性能比 pthread\_mutex 还要高，但一旦有等待情况出现时，性能就会下降许多。相对于 OSSpinLock 来说，它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说，它比较合适。

dispatch\_semaphore是属于闲等待，CPU不会消耗，因此，在做磁盘缓存的时候，用时较长，需要等待的话，会比较节省资源

OSSpinLock 自旋锁，不安全，所以使用了dispatch\_semaphore进行处理，因为大文件，需要等待的情况较多

2.大文件处理

* YYKVStorage 通过key value的一个封装，实现增删改查
* YYKVStorage 实现细节:

提供对应的sqlite3的db增删改查的接口，在操作的过程都做了防御式的操作

```
- (BOOL)_dbClose {
// 关闭数据库
if (!_db) {
return YES;
}

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

if (_dbStmtCache) {
_dbStmtCache = NULL;
}

do {
retry = NO;
result = sqlite3_close(_db);

if (result == SQLITE_BUSY || result == SQLITE_LOCKED) {
// 如果sqlite被占用，那进行stmt的析构操作，让其释放资源
// 在重新试一次
if (!stmtFinalized) {
stmtFinalized = YES; //析构
sqlite3_stmt *stmt;
// sqlite3_stmt 是一种辅助数据结构，用于操作二进制数据
// 循环释放所有的sqlite stmt
while ((stmt = sqlite3_next_stmt(_db, nil))) {
sqlite3_finalize(stmt); //析构 所有的sqlite stmt的辅助
retry = YES; // 然后重试，
}
}
} else if (result != SQLITE_OK) {
// 关闭失败
// 输出 错误日志
NSLog(@"关闭失败");
}

} while (retry);

_db = NULL;
return YES;

}

- (BOOL)_dbCheck {
if (_db) {
// 如果重试错误的次数小于限定次数，并且大于最小重试时间，则重新进行打开和初始化检查
if (_dbOpenErrorCount < kMaxErrorRetryCount &&
CACurrentMediaTime() - _dbLastOpenErrorTime > kMinRetryTimeInterval) {
return [self _dbOpen] && [self _dbInitialize];
} else {
return NO;
}
}
return YES; // 正常
}
```

对sqlite的stmt也做了缓存，加速其性能

```
// 获取缓存中的stmt，用sql做key
- (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) {
// 如果stmt为空的话
int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
if (result != SQLITE_OK) {
// 如果不成功
NSLog(@"创建stmt失败");
return NULL; // 返回空
}
CFDictionarySetValue(_dbStmtCache, sql.UTF8String, stmt); // 缓存起来，都是同一个stmt，每次创建的话，会大量消耗资源，因此这里做了缓存
} else {
sqlite3_reset(stmt);
}

return stmt;
}
```

存储方式有三个模式

1.文件 2.sqlite3 3.混合 // 混合的情况有个阈值，大于阈值，则存文件

* db sqlite3的使用细节

sqlite3\_stmt 操作数据的辅助数据接口，用于执行sql，并且返回结果 stmt 也进行做了缓存，因为这个sql会重复不定期使用

* 优化: 可以升级最新版本的sqlite3，以此提高效率

## YYCache

提供了增删改查的API，底下调用的YYMemoryCache和YYDiskCache，封装了一些基本逻辑。在save的时候，会保存到YYMemoryCache和YYDiskCache，读取的时候，会优先读取YYMemoryCache实现高速缓存，再读取低速缓存。
