文章

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 进行文件缓存 和 数据库缓存,提供了一整套接口,让我们使用时就像使用字典一样方便
本文由作者按照 CC BY 4.0 进行授权