YYCache终结篇

来源:互联网 时间:2017-01-22


先来看看YYCache整体框架设计图。



YYCache框架图.jpeg
(一)简单来分析下这个框架

从上图可以很清晰的看出这个框架的基础框架为:
交互层-逻辑层-处理层。
无论是最上层的YYCache或下面两个独立体YYMemoryCache和YYDiskCache都是这样设计的。逻辑清晰。
来看看YYMemoryCache,逻辑层是负责对交付层的一个转接和对处理层发布一些命令,比如增删改查。而处理层则是对逻辑层发布的命令进行具体对应的处理。组件层则是对处理层的一种补充,也是元数据,扩展性很好。详解可以看上一篇。YYDiskCache也是如此做法。这两个类我们都是可以单独拿出来使用的。而多添加一层YYCache,则可以让我们使用的更加方便,自动为我们同时进行内存缓存和磁盘缓存,读取的时候获得内存缓存的高速读取。代码如下。在YYCache.m中


- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
[_memoryCache setObject:object forKey:key];
[_diskCache setObject:object forKey:key];
}
- (id<NSCoding>)objectForKey:(NSString *)key {
id<NSCoding> object = [_memoryCache objectForKey:key];
if (!object) {
object = [_diskCache objectForKey:key];
if (object) {
[_memoryCache setObject:object forKey:key];
}
}
return object;
}

可以说作者在高内聚低耦合中做得非常好。


(二)关于YYDiskCache

由于YYMemoryCache已经在上一篇中分析过,这里就不做介绍了。那就简单的来看看YYDiskCache吧。大体上也和YYMemoryCache差不多,使用LRU淘汰算法,使用cost, count, and age来做limit。具体上的实现当然会不同,YYMemoryCache是使用双链表和字典来做操作,而YYDiskCache使用文件和数据库。在这里引用下作者的一句。


iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。


所以有了这样的一个枚举。


typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0,//只存于文件
YYKVStorageTypeSQLite = 1,//只存于数据库.
YYKVStorageTypeMixed = 2,//当大于20k(默认,可以设置),两者都存。
};

我们怎样才能使用到这个YYKVStorageType呢?
在YYDiskCache.h中的实例方法中有提供


//@param path 具体的可以写数据的路径,例如path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]/name
//@param threshold 当为0的时候就是YYKVStorageTypeFile,当为NSUIntegerMax的时候是YYKVStorageTypeSQLite,其他是YYKVStorageTypeMixed
- (nullable instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER;

可以了解下上面的NS_DESIGNATED_INITIALIZER,大概的意思就是其余的init都要使用到这个init。
下面大概讲下YYDiskCache存储object的过程。需要注意的是这里的object需要遵循NSCoding。而YYMemoryCache中的不需要。


- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;

- (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;
//如果大于20kb,则提供文件存储
if (_kv.type != YYKVStorageTypeSQLite) {
if (value.length > _inlineThreshold) {
filename = [self _filenameForKey:key];
}
}
//发布存储命令
Lock();//线程安全
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}

在YYKVStorage处理存储命令


- (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;
}
//如果大于20kb,则使用mix存储,
if (filename.length) {
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
[self _fileDeleteWithName:filename];//数据库存储失败,则同样需要删除已存储在文件的数据,为了能够使用limit规则
return NO;
}
return YES;
} else {
//如果<20kb,并且不是YYKVStorageTypeSQLite 则需要删除文件存储的,因为这种情况不符合情理
if (_type != YYKVStorageTypeSQLite) {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
//使用数据库存储
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}

以上则是YYKVStorage存储过程。


下面来说下删除,举个limit age规则来讲吧


//@param age 存活时间多少秒
- (void)trimToAge:(NSTimeInterval)age;
- (void)trimToAge:(NSTimeInterval)age {
Lock();//信号锁
[self _trimToAge:age];
Unlock();
}
- (void)_trimToAge:(NSTimeInterval)ageLimit {
//存活时间为0全部删除
if (ageLimit <= 0) {
[_kv removeAllItems];
return;
}
//获取age的unix时间戳,发布删除命令
long timestamp = time(NULL);
if (timestamp <= ageLimit) return;
long age = timestamp - ageLimit;
if (age >= INT_MAX) return;
[_kv removeItemsEarlierThanTime:(int)age];
}

在YYKVStorage处理删除命令


- (BOOL)removeItemsEarlierThanTime:(int)time {
if (time <= 0) return YES;
if (time == INT_MAX) return [self removeAllItems];
switch (_type) {
case YYKVStorageTypeSQLite: {
if ([self _dbDeleteItemsWithTimeEarlierThan:time]) {
[self _dbCheckpoint];
return YES;
}
} break;
case YYKVStorageTypeFile:
case YYKVStorageTypeMixed: {
//注意mix的时候 ,需要同时删除文件和数据库的。这就是元数据的好处,提供了文件名,时间,等信息。可以让文件也能如果根据时间来操作,实现LRU.
NSArray *filenames = [self _dbGetFilenamesWithTimeEarlierThan:time];
for (NSString *name in filenames) {
[self _fileDeleteWithName:name];
}
if ([self _dbDeleteItemsWithTimeEarlierThan:time]) {
[self _dbCheckpoint];
return NO;
}
} break;
}
return NO;
}

以上则是根据age来删除数据。


在数据库方面也用了一些小优化。比如,添加最后一次访问时间last_access_time的索引,根据last_access_time来查询的时候会快些,也采用了分页查询。看下代码。在初始化数据库的时候,


- (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)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;
do {
int perCount = 16;
//NSString *sql = @"select key, filename, size from manifest order by last_access_time asc limit ?1;";
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;
}

下面来讲下文件这块。
所用到的文件有。


static NSString *const kDBFileName = @"manifest.sqlite";//这是数据库文件,reset的时候会删除,但是如果用到会自动创建
static NSString *const kDBShmFileName = @"manifest.sqlite-hm";//数据库衍生文件,reset的时候会删除,但是如果用到会自动创建
static NSString *const kDBWalFileName = @"manifest.sqlite-wal";//数据库衍生文件,reset的时候会删除,但是如果用到会自动创建
static NSString *const kDataDirectoryName = @"data";//data存储文件,不会删除,清除数据的时候会转移到trash文件的子文件。
static NSString *const kTrashDirectoryName = @"trash";//垃圾存储文件,不会删除,其下的子文件会被删除。

来看一下_YYDiskCacheGetGlobal ,


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

这个cache会用一个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];
});
}

所以那个data文件和trash不能删除,只能转移到trash子文件下清空。


(三)关于YYCache的使用。

1.如果不太在乎limit规则的,并且object遵循NSCoding的,建议使用YYCache
2.如果只用于内存缓存且不遵循NSCoding的,只能使用YYMemoryCache。
3.如果是文件存储的,且用到limit规则的,建议使用YYDiskCache。


(挖坑,下一篇会讲关于YYImage的一些东西。)




相关阅读:
Top