YYCache源码阅读:YYKVStorage

YYDiskCache 磁盘缓存

YYDiskCache是一个线程安全的缓存,用于存储SQLite支持的键值对和文件系统(类似于NSURLCache的磁盘缓存)。

YYDiskCache具有以下功能:

  1. 它使用LRU(最近最少使用)来删除对象。
  2. 它可以通过成本,计数和年龄来控制。
    3 .它可以配置为在没有可用磁盘空间时自动驱逐对象。
  3. 它可以自动决定每个对象的存储类型(sqlite / file)。

所以 YYDiskCache 磁盘缓存又分为 文件缓存 和 数据库缓存

1
2
3
4
5
/// 根据这个属性进行区分,当大于该值时使用数据库缓存,小于该值使用文件缓存
/// 默认 20480(20kb)
/// 0 文件缓存
/// NSUIntegerMax 数据库缓存
@property (readonly) NSUInteger inlineThreshold;

NSMapTable

YYDiskCache 维护一个 NSMapTable(Path : YYDiskCache) 来进行缓存操作

NSMapTable: 类似于 NSDictionary 但是可以指定key-value的引用类型,当value的引用类型为 weak 时,当 value 为 nil 时,自动删除key-value

1
2
3
4
5
6
/// 初始化
_globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
/// set
[_globalInstances setObject:cache forKey:cache.path];
/// get
id cache = [_globalInstances objectForKey:path];

信号量 dispatch_semaphore

YYDiskCache 使用信号量进行加锁解锁操作。

dispatch_semaphore_t GCD 中信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量 +1;每当发送一个等待信号时信号量 -1,;如果信号量为 0 则信号会处于等待状态,直到信号量大于 0(或者超时) 开始执行之后代码。

YYDiskCache

YYDishCache 使用 YYKVStorage 进行文件缓存处理,并自定义了淘汰算法,删除那些最近时间段内不常用的对象。类似于 YYMemoryCache 与 _YYLinkMap 的关系。

1
2
3
4
5
@implementation YYDiskCache {
YYKVStorage *_kv;
dispatch_semaphore_t _lock;
dispatch_queue_t _queue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 需要通过Path进行初始化
- (instancetype)init {
@throw [NSException exceptionWithName:@"YYDiskCache init error" reason:@"YYDiskCache must be initialized with a path. Use 'initWithPath:' or 'initWithPath:inlineThreshold:' instead." userInfo:nil];
return [self initWithPath:@"" inlineThreshold:0];
}

// 默认边界存储值为 20KB,超出20KB 使用数据库存储,否则文件存储
- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}

- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
self = [super init];
if (!self) return nil;

// 根据path获取cache,有的话就直接返回
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
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; // 60s轮询检查一遍内存

// 轮询内存检查
[self _trimRecursively];
// 将自身存储于NSMapTable中
_YYDiskCacheSetGlobal(self);

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
return self;
}

边界检查

YYDiskCache 在初始化时也会进行一个递归调用的边界检查任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (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]; // 递归调用
});
}

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

分别调用 YYKVStorage 的相应方法

注意

  • YYDiskCache 默认将数据归档成 Data 然后操作 Data 数据进行缓存
  • 如果实现了归档协议则使用归档协议进行存储

YYKVStorage

YYKVStorage是一个基于sqlite和文件系统的键值存储。但作者不建议我们直接使用此类(ps:这个类被封装到了YYDiskCache里,可以通过YYDiskCache间接使用此类),这个类只有一个初始化方法,即initWithPath:type:,初始化后,讲根据path创建一个目录来保存键值数据,初始化后,如果没有得到当前类的实例对象,就表示你不应该对改目录进行读写操作。最后,作者还写了个警告,告诉我们这个类的实例对象并不是线程安全的,你需要确保同一时间只有一条线程访问实例对象,如果你确实需要在多线程中处理大量数据,可以把数据拆分到多个实例对象当中去。

YYKVStorage: YYDiskCache 操作数据的工具类。 YYKVStorage 实现了 文件缓存 和 数据库缓存 功能。可以像操作字典一样使用 key-value 操作数据。

YYKVStorageType 枚举类型,代表使用哪个方式进行存储

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
// 使用文件缓存 当 YYDiskCache 的 inlineThreshold 为 O 时
YYKVStorageTypeFile = 0,
// 使用数据库缓存 当 YYDiskCache 的 inlineThreshold 为 NSUIntegerMax 时
YYKVStorageTypeSQLite = 1,
// 同时使用数据库和文件缓存,当大于 YYDiskCache 的 inlineThreshold 时使用数据库缓存,小于使用文件缓存
YYKVStorageTypeMixed = 2,
};

YYKVStorageItem: YYKVStorage 操作的数据

1
2
3
4
5
6
7
8
9
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; // 缓存数据的key
@property (nonatomic, strong) NSData *value; // 缓存数据
@property (nullable, nonatomic, strong) NSString *filename; // 缓存文件名(文件缓存时有,数据库缓存没有,有可能是数据库缓存但是仍然存在,应为没大于边界值时使用的文件缓存)
@property (nonatomic) int size; // 数据大小
@property (nonatomic) int modTime; // 修改时间
@property (nonatomic) int accessTime; // 访问时间
@property (nullable, nonatomic, strong) NSData *extendedData; // 附加数据,通过关联存储在YYDiskCache中
@end

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type {
...
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;
... 文件夹创建

// _dbOpen 方法创建和打开数据库 manifest.sqlite
// 调用 _dbInitialize 方法创建数据库中的表
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]; // 清空回收站
return self;
}

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
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)
);
create index if not exists last_access_time_idx on manifest(last_access_time);
*/

增删查改

    1. 如果存文件类型且没有文件名,则直接返回NO
    2. 如果有文件名
      1. 如果存文件失败,直接返回NO
      2. 如果存数据库失败(将filename作为value处在数据库里面),则删除文件,并返回NO
      3. 存文件,存数据库都成功,返回yes
    3. 文件名不存在,存数据库类型或者混合存储类型

      1. 不是存数据库类型,获取文件名,删除文件
      2. 存数据库
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      - (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
      ...

      // 文件名存在 (>inlineThreshold)
      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) {
      NSString *filename = [self _dbGetFilenameWithKey:key];
      if (filename) {
      [self _fileDeleteWithName:filename];
      }
      }
      return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
      }
      }
    1. 如果是数据库类型,直接删除数据库里面的数据
    2. 如果是存文件,或者混合类型
      1. 从数据库获取filename
      2. 如果文件存在,删除文件
      3. 删除数据库里面的值
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        - (BOOL)removeItemForKey:(NSString *)key {
        ...
        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;
        // 删除时间超了的数据
        - (BOOL)removeItemsEarlierThanTime:(int)time;
        // 当数据总大小大于临界值时删除
        - (BOOL)removeItemsToFitSize:(int)maxSize;
        // 当数据个数大于临界值时执行删除操作
        - (BOOL)removeItemsToFitCount:(int)maxCount;
        - (BOOL)removeAllItems;
    1. 文件类型
      1. 通过key从数据库获取文件名
      2. 通过文件名获取数据
      3. 如果没有数据,从数据库删除这条数据
    2. 数据库类型
      1. 从数据库根据key获取value data数据
    3. 混合类型
      1. 先获取文件名
      2. 文件名存在,根据文件名获取文件数据,不存在,则删除数据库中数据
      3. 文件名不存在,直接从数据可获取数据
    4. 如果获取到值,则更新数据访问时间

DB操作

_db... 直接操作sqlite数据库,又想了解的自己去看,挺全面的

总结

  • YYDiskCache 使用 YYKVStorage 进行文件缓存 和 数据库缓存,提供了一整套接口,让我们使用时就像使用字典一样方便


-------------The End-------------

本文标题:YYCache源码阅读:YYKVStorage

文章作者:kysonyangs

发布时间:2018年12月16日 - 20:12

最后更新:2020年05月17日 - 18:05

原始链接:https://kysonyangs.github.io/default/YYCache源码阅读2/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。