YYCache源码分析(二) - YYDiskCache
YYDiskCache
YYDiskCache采用的 SQLite 配合文件的存储方式,当单条数据小于 20K 时进行数据库缓存,数据越小 SQLite 读取性能越高;单条数据大于 20K 时进行文件缓存。
YYDiskCache.m方法实现
YYDiskCache的数据结构:
@implementation YYDiskCache {
YYKVStorage *_kv; // 对数据进行缓存操作的对象
dispatch_semaphore_t _lock; // 同步锁,每次仅允许一个线程操作
dispatch_queue_t _queue; // 执行block的队列
}
YYDiskCache中需要注意到的数据结构和函数:
/// weak reference for all instances
static NSMapTable *_globalInstances; // 全局字典,用来存放YYDiskCache对象
static dispatch_semaphore_t _globalInstancesLock; //互斥锁,保证每次仅一个线程方法全局字典
// 初始化字典和互斥锁
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];
});
}
// 获取YYDiskCache对象
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;
}
// 保存YYDiskCache对象
static void _YYDiskCacheSetGlobal(YYDiskCache *cache) {
if (cache.path.length == 0) return;
_YYDiskCacheInitGlobal();
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
[_globalInstances setObject:cache forKey:cache.path];
dispatch_semaphore_signal(_globalInstancesLock);
}
每一条存储路径下,都对应一个YYDiskCache对象,不同的YYDiskCache都共享一个NSMapTable集合。在创建某条路径的YYDiskCache对象时,会首先查找集合,若该路径下的YYDiskCache对象存在,则从集合中获取。若没有,则重新创建。这样在不通路径下切换时,节省了大量时间。
NSMapTable对于NSDictionary对比:NSMapTable可以指定key/value是需要strong,weak,甚至是copy,如果使用的是weak,当key、value在被释放的时候,会自动从NSMapTable中移除这一项。NSMapTable中可以包含任意指针,使用指针去做检查操作。
NSDcitionary或者NSMutableDictionary中对于key和value的内存管理是:对key进行copy,对value进行强引用。NSDcitionary中对于key的类型,是需要key支持NSCopying协议,并且在NSDictionary中,object是由“key”来索引的,key的值不能改变,为了保证这个特性在NSDcitionary中对key的内存管理为copy,在复制的时候需要考虑对系统的负担,因此key应该是轻量级的,所以通常我们都用字符串和数字来做索引,但这只能说是key-to-object映射,不能说是object-to-object的映射。
NSMapTabTable更适合于我们一般所说的映射标准,它既可以处理key-to-value又可以处理object-to-object
YYDiskCache中实现定时清理缓存的方式与YYMemoryCache一样,首先在初始化中调用_trimRecursively方法。_trimRecursively方法的实现就是递归和dispatch_after结合的方式。
// 根据key值创建缓存文件名
- (NSString *)_filenameForKey:(NSString *)key {
NSString *filename = nil;
// 自定义block的到文件名
if (_customFileNameBlock) filename = _customFileNameBlock(key);
// md5加密得到文件名
if (!filename) filename = key.md5String;
return filename;
}
// 清理大小为targetFreeDiskSpace的磁盘空间
- (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace {
if (targetFreeDiskSpace == 0) return;
// 磁盘已缓存的大小
int64_t totalBytes = [_kv getItemsSize];
if (totalBytes <= 0) return;
// 磁盘可用空间大小
int64_t diskFreeBytes = _YYDiskSpaceFree();
if (diskFreeBytes < 0) return;
// 磁盘需要清理的大小 = 目标要清除的空间大小 - 可用空间大小
int64_t needTrimBytes = targetFreeDiskSpace - diskFreeBytes;
if (needTrimBytes <= 0) return;
// 磁盘缓存的空间大小限制 = 已缓存大小 - 需要清理的空间大小
int64_t costLimit = totalBytes - needTrimBytes;
if (costLimit < 0) costLimit = 0;
[self _trimToCost:(int)costLimit];
}
// 磁盘空闲的大小
static int64_t _YYDiskSpaceFree() {
NSError *error = nil;
// 获取主目录的文件属性,主目录下包含Document、Liberary等目录
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfFileSystemForPath:NSHomeDirectory() error:&error];
if (error) return -1;
// 获取主目录的可用空间,即磁盘的可用空间
int64_t space = [[attrs objectForKey:NSFileSystemFreeSize] longLongValue];
if (space < 0) space = -1;
return space;
}
// 初始化字典(用来缓存YYDiskCache对象)与创建锁
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];
});
}
// 保存新的YYDiskCache
static void _YYDiskCacheSetGlobal(YYDiskCache *cache) {
if (cache.path.length == 0) return;
_YYDiskCacheInitGlobal();
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
[_globalInstances setObject:cache forKey:cache.path];
dispatch_semaphore_signal(_globalInstancesLock);
}
// 获取已经缓存的YYDiskCache对象
static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) {
if (path.length == 0) return nil;
// 初始化字典(用来缓存YYDiskCache对象)与创建锁
_YYDiskCacheInitGlobal();
// 加锁
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
id cache = [_globalInstances objectForKey:path];
// 解锁
dispatch_semaphore_signal(_globalInstancesLock);
return cache;
}
初始化
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
self = [super init];
if (!self) return nil;
// 1.根据path先从缓存里面找YYDiskCache(未找到再去重新创建实例)
YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
if (globalCache) return globalCache;
// 2.重新创建实例
// 2.1创建cache,设置缓存类型
/**
path:缓存路径
threshold:如果缓存数据>threshold,存储成文件,相反则存储到数据库,threshold默认20480kb
threshold == 0:数据存储成单独的文件
threshold == NSUIntegerMax:数据全部存储到数据库
*/
YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}
// 2.2实例化YYKVStorage对象(YYKVStorage上面已分析,YYDiskCache的缓存实现都在YKVStorage)
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
if (!kv) return nil;
// 2.3初始化数据
_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;
//清理缓存
[self _trimRecursively];
// 2.4放入NSMapTable中缓存
_YYDiskCacheSetGlobal(self);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
return self;
}
添加缓存
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
if (!key) return;
if (!object) {
// 缓存对象为null,删除缓存
[self removeObjectForKey:key];
return;
}
//获取扩展数据,用户可以在存储缓存数据前调用类方法设置扩展数据
NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
NSData *value = nil;
/* 自定义block进行归档操作
_customArchiveBlock != nil时,将调用_customArchiveBlock将对象转成NSData类型数据
_customArchiveBlock的主要目的是:_customArchiveBlock代替不支持NSCoding协议的对象的归档
*/
if (_customArchiveBlock) {
value = _customArchiveBlock(object);
} else { // 系统归档的方式
@try {
value = [NSKeyedArchiver archivedDataWithRootObject:object];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (!value) return;
NSString *filename = nil;
// 缓存方式不为sqlite类型且数据大小超过规定值,获取文件名
if (_kv.type != YYKVStorageTypeSQLite) {
if (value.length > _inlineThreshold) {
filename = [self _filenameForKey:key];
}
}
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}