高性能 iOS 缓存框架。
(该项目是 YYKit 组件之一) github:https://github.com/ibireme/YYCache
特性
LRU: 缓存支持 LRU (least-recently-used) 淘汰算法。
缓存控制: 支持多种缓存控制方法:总数量、总大小、存活时间、空闲空间。
兼容性: API 基本和 NSCache 保持一致,所有方法都是线程安全的。
内存缓存
对象释放控制: 对象的释放(release)可以配置为同步或异步进行,可以配置在主线程或后台线程进行。
自动清空: 当收到内存警告或 App 进入后台时,缓存可以配置为自动清空。
磁盘缓存
可定制性: 磁盘缓存支持自定义的归档解档方法,以支持那些没有实现 NSCoding 协议的对象。
存储类型控制: 磁盘缓存支持对每个对象的存储类型 (SQLite/文件) 进行自动或手动控制,以获得更高的存取性能。
思考
YYCache是如何把数据写入内存之中的?又是如何实现的高效读取?
YYCache采用了何种方式把数据写入磁盘?
YYMemoryCache(内存缓存)
使用YYMemoryCache可以把数据缓存进内存之中,它内部会创建了一个YYMemoryCache对象,然后把数据保存进这个对象之中。
但凡涉及到类似这样的操作,代码都需要设计成线程安全的。所谓的线程安全就是指充分考虑多线程条件下的增删改查操作。
YYMemoryCache内部封装了一个对象_YYLinkedMap,包含了下边这些属性:
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // MRU, do not change it directly
_YYLinkedMapNode *_tail; // LRU, do not change it directly
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}
可以看出来,CFMutableDictionaryRef _dic将被用来保存数据。这里使用了CoreFoundation的字典,性能更好。字典里边保存着的是_YYLinkedMapNode对象。
/**
A node in linked map.
Typically, you should not use this class directly.
*/
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
YYMemoryCache使用了LRU淘汰算法,也就是当数据超过某个限制条件后,我们会从链表的尾部开始删除数据,直到达到要求为止。
我们简单看一段把一个节点插入到最开始位置的代码:
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
if (_head == node) return;
if (_tail == node) {
_tail = node->_prev;
_tail->_next = nil;
} else {
node->_next->_prev = node->_prev;
node->_prev->_next = node->_next;
}
node->_next = _head;
node->_prev = nil;
_head->_prev = node;
_head = node;
}
如果有一列数据已经按顺序排好了,我使用了中间的某个数据,那么就要把这个数据插入到最开始的位置,这就是一条规则,越是最近使用的越靠前。
YYKVStorage
YYKVStorage让我们只关心3件事:
- 数据保存的路径
- 保存数据,并为该数据关联一个key
- 根据key取出数据或删除数据
对于初始化方法而言,如果某个类需要提供一个指定的初始化方法,那么就要使用NS_DESIGNATED_INITIALIZER给予提示。同时使用UNAVAILABLE_ATTRIBUTE禁用掉默认的方法。接下来要重写禁用的初始化方法,在其内部抛出异常:
- (instancetype)init {
@throw [NSException exceptionWithName:@"YYKVStorage init error" reason:@"Please use the designated initializer and pass the 'path' and 'type'." userInfo:nil];
return [self initWithPath:@"" type:YYKVStorageTypeFile];
}
千万不要怕程序抛出异常,在发布之前,能够发现潜在的问题是一件好事。
分析YYKVStorage.m的代码:

- 每个函数只实现先单一功能,函数组合使用形成新的功能
- 对于类内部的私有方法,前边添加_
- 使用预处理stmt对数据库进行了优化,避免不必要的开销
- 健壮的错误处理机制
- 可以说是使用iOS自带sqlite3的经典代码,在项目中可以直接拿来用
它内部使用了两种方式保存数据:一种是保存到数据库中,另一种是直接写入文件。当数据较大时,使用文件写入性能更好,反之数据库更好。
YYDiskCache
YYDiskCache的接口设计在YYKVStorage的基础上添加了一些新的特性。如:
/**
If this block is not nil, then the block will be used to archive object instead
of NSKeyedArchiver. You can use this block to support the objects which do not
conform to the `NSCoding` protocol.
The default value is nil.
*/
@property (nullable, copy) NSData *(^customArchiveBlock)(id object);
/**
If this block is not nil, then the block will be used to unarchive object instead
of NSKeyedUnarchiver. You can use this block to support the objects which do not
conform to the `NSCoding` protocol.
The default value is nil.
*/
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data);
使用上边的属性可以设置对象与NSData之间转化的规则,这和很多框架一样,目的是给该类增加一些额外的特性。
设计一个存储类,需要考虑下边几个特性:
- 标识,在YYDiskCache中使用path作为存储位置的标识,使用key作为value的标识
- 操作方法 包含增删改查
- 限制条件 包括count,cost,age
- 其他
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内部实现了一种这样的机制,他会把开发者创建的每一个YYDiskCache对象保存到一个全局的集合中,YYDiskCache根据path创建,如果开发者创建了相同path的YYDiskCache,那么就会返回全局集合中的YYDiskCache。
这里就产生了一个很重要的概念,在全局对象中的YYDiskCache是可以释放的。为什么会发生这种事呢?按理说全局对象引用了YYDiskCache,它就不应该被释放的。这个问题我们马上就会给出答案。
继续分析上边的代码:
static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path)这种风格的代码是值得学习的第一点,如果在一个文件中,有一些方法是不依赖某个对象的,那么我们就可以写成这种形式,它可以跨对象调用,因此这算是私有函数的一种写法吧。
if (path.length == 0) return nil;这个不用多说,健壮的函数内部都要有检验参数的代码。
_YYDiskCacheInitGlobal();从函数的名字,我们可以猜测出它是一个初始化全局对象的方法,它内部引出了一个很重要的对象:
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];
});
}
YYCache
当我们读到YYCache的时候,感觉一下子就轻松了很多,YYCache就是对YYMemoryCache和YYDiskCache的综合运用,创建YYCache对象后,就创建了一个YYMemoryCache对象和一个YYDiskCache对象。唯一新增的特性就是可以根据name来创建YYCache,内部会根据那么来创建一个path,本质上还是使用path定位的。
该文主要参考:马在路上
网友评论