☞源码: YYCache
☞ 作者:YYCache 设计思路
☞ 阅读代码的注释:github.com/wenghengcon…
YYCache
YYCache 是由内存缓存 YYMemoryCache
与磁盘缓存 YYDiskCache
组成的Cache矩阵,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。
在 YYCache 中使用接口访问缓存对象时,会先去尝试
- 从内存缓存
YYMemoryCache
中访问- 命中:之前访问过,且仍然在内存中,任何击中对象。
- 未命中->
YYDiskCache
:之前未使用过该key缓存的对象,或者由于各种限制驱逐了该缓存对象。- 命中:任何击中对象,更新到内存缓存。
- 未命中:缓存到磁盘中。
@interface YYCache : NSObject
@property (copy, readonly) NSString *name;
@property (strong, readonly) YYMemoryCache *memoryCache;
@property (strong, readonly) YYDiskCache *diskCache;
- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
- (BOOL)containsObjectForKey:(NSString *)key;
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key;
- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;
.....
@end
复制代码
缓存策略
LRU,即LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
在YYCache
中,有两种实现:
内存缓存中,采用双链表的实现:
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当需要清除缓存时,将链表尾部的数据丢弃。
在磁盘缓存中,采用了SQLite的实现:
- 新数据插入到缓存表;
- 每当缓存命中(即缓存数据被访问),则更新该缓存对应的表中数据行对应的
访问时间
; - 当需要清除缓存时,根据
访问时间
将最旧的条目删除;
内存缓存
YYMemoryCache
YYMemoryCache 是一个高速的内存缓存,用于存储键值对。它与 NSDictionary 相反,Key 被保留并且不复制。API 和性能类似于 NSCache,所有方法都是线程安全的。
它是通过双链表实现LRU算法、**CFMutableDictionaryRef**
** 实现快速存取、****pthread_mutex_lock**
来保证线程安全。
与 NSCache 的不同之处在于:
YYMemoryCache | NSCache | |
---|---|---|
驱逐 | LRU(least-recently-used) 算法来驱逐对象 | 非确定性 |
控制 | age、cost、count | 不精确的 |
异常 | 内存警告或者 App 进入后台时自动驱逐对象 | 无 |
API
优雅简洁的API:
@interface YYMemoryCache : NSObject
#pragma mark - Attribute
@property (nullable, copy) NSString *name;
@property (readonly) NSUInteger totalCount;
@property (readonly) NSUInteger totalCost;
#pragma mark - Limit
@property NSUInteger countLimit;
@property NSUInteger costLimit;
@property NSTimeInterval ageLimit;
@property NSTimeInterval autoTrimInterval;
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
@property BOOL releaseOnMainThread;
@property BOOL releaseAsynchronously;
#pragma mark - Access Methods
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;
#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
@end
@implementation YYMemoryCache {
pthread_mutex_t _lock; //互斥锁,保证线程安全,对于所有的属性和方法
_YYLinkedMap *_lru; //_YYLinkedMap 处理层类。处理链表操作,操作缓存对象
dispatch_queue_t _queue; //串行队列,用于用于 YYMemoryCache 的 trim 操作
}
复制代码
LRU缓存策略
LRU缓存策略的实现,依靠双链表_YYLinkedMap,_YYLinkedMapNode是链表节点。策略是:
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当需要清除缓存时,将链表尾部的数据丢弃。
@interface _YYLinkedMapNode : NSObject {
@package
//__unsafe_unretained:和__weak 一样,唯一的区别便是,对象即使被销毁,指针也不会自动置空, 此时指针指向的是一个无用的野地址。如果使用此指针,程序会抛出 BAD_ACCESS 的异常。
// __unsafe_unretained 在于性能优化,节点被 _YYLinkedMap 的 _dic 强引用
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@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;
}
复制代码
链表的操作
(void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
if (!key) return;
if (!object) {
//设置nil,就移除对应的Node
[self removeObjectForKey:key];
return;
}
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
NSTimeInterval now = CACurrentMediaTime();
if (node) {
_lru->_totalCost -= node->_cost; //移除掉老的cost
_lru->_totalCost += cost; //增加新的cost
node->_cost = cost;
node->_time = now;
node->_value = object;
[_lru bringNodeToHead:node]; //将访问过的,放在表头
} else {
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
[_lru insertNodeAtHead:node];
}
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
if (_lru->_totalCount > _countLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
//持有node,等待在该队列上释放
[node class]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //hold and release in queue
});
}
}
pthread_mutex_unlock(&_lock);
}
- (void)removeObjectForKey:(id)key {
if (!key) return;
pthread_mutex_lock(&_lock);
//将要移除的node
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
//移除node
[_lru removeNode:node];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
//持有node,等待在该队列上释放
[node class]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //异步释放
});
}
}
pthread_mutex_unlock(&_lock);
}
.....
复制代码
驱逐缓存对象
驱逐对象有三个维度:cost、count、age,分别代表每个对象的消耗、对象的总个数、对象的过期时间
调用
#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
复制代码
以Count为例:
- (void)_trimToCount:(NSUInteger)countLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
if (countLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCount <= countLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;
NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_totalCount > countLimit) {
// 从双线链表的尾节点(LRU)开始清理缓存,释放资源
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
usleep(10 * 1000); //10 ms
}
}
if (holder.count) {
// 判断是否需要在主线程释放,采取释放缓存对象操作
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // 异步释放
});
}
}
复制代码
定时
- (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];
});
}
复制代码
上边的代码中,每隔_autoTrimInterval
时间就会在后台尝试处理数据,然后再次调用自身,这样就实现了一个类似定时器的功能。
异步释放
关于上面的异步释放缓存对象的代码,需要着重说明:
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // release in queue
});
复制代码
这个技巧 ibireme 在他的另一篇文章 iOS 保持界面流畅的技巧 中有提及:
Note: 对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
而上面代码中的 YYMemoryCacheGetReleaseQueue 这个队列为:
// 静态内联 dispatch_queue_t
static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
}
复制代码
在源码中可以看到 YYMemoryCacheGetReleaseQueue 是一个低优先级 DISPATCH_QUEUE_PRIORITY_LOW
队列,可以让 iOS 在系统相对空闲时再来异步释放缓存对象。
- 让block来retain一下对象?
应该是holder在执行完这个方法后就出了作用域了,reference会减1,但是此时holder不会被dealloc,因为block 中retain了node,使得holder的reference count为1,当执完block后,holder的reference count又-1,此时node就会在block对应的queue上release了。
2. 为什么需要在指定queue销毁对象?
放到其他线程去销毁对象,减轻当前线程压力,一般都是减轻main thread的压力
快速访问
YYMemoryCache之所以能做到高速命中缓存,采用的还是NSDictionary的方式,但是其采用的是CFMutableDictionaryRef
而不是NSMutableDictionary
,有两个考虑:
CFMutableDictionaryRef
性能更优;NSMutableDictionary
中的键是被拷贝的并且需要是不变的。如果在一个键在被用于在字典中放入一个值后被改变的话,那么这个值就会变得无法获取了。一个有趣的细节是,在 中键是被 copy 的,但是在使用一个 toll-free 桥接的CFMutableDictionaryRef
时却只会被 retain。
CFMutableDictionaryRef
的用法
//create
_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
//setter
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
//getter
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
//contains
BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
//remove
CFDictionaryRemoveValue(\_dic, (__bridge const void *)(node->_key));
复制代码
线程安全
在内存缓存中,用pthread_mutex_t
来保证线程安全,具体可参考文档:
在读写缓存时,先加锁,再解锁:
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
pthread_mutex_lock(&_lock);
//写缓存数据
pthread_mutex_unlock(&_lock);
}
- (id)objectForKey:(id)key {
pthread_mutex_lock(&_lock);
//读缓存数据
pthread_mutex_unlock(&_lock);
}
复制代码
但是在几种驱逐缓存对象时,却采用pthread_mutex_trylock
来进行加锁,两者的区别在于:
pthread_mutex_trylock behaves identically to pthread_mutex_lock, except that it does not block the calling thread if the mutex is already locked by another thread (or by the calling thread in the case of a “fast” mutex). Instead, pthread_mutex_trylock returns immediately with the error code EBUSY.
From www.skrenta.com/rt/man/pthr…
// 加锁(阻塞操作)
pthread_mutex_lock(pthread_mutex_t *mutex);
// 试图加锁(不阻塞操作)
// 当互斥锁空闲时将占有该锁;否则立即返回
// 但是与pthread_mutex_lock不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。
pthread_mutex_trylock(pthread_mutex_t *mutex);
为什么要这样做?
因为读写缓存,一定要保证线程安全,必须阻塞,但是删减缓存对象时,为了提高性能,将这种低优先级
、定时
的操作进行尝试加锁,以避免因线程加锁带来的损耗。
磁盘缓存
YYDiskCache
YYDiskCache 是一个线程安全的磁盘缓存,用于存储由 SQLite 和文件系统支持的键值对。
SQLite数据库表来实现LRU策略,dispatch_semaphore
保证线程安全,通过SQLite的高性能和SQL语句缓存_dbStmtCache
(是CFMutableDictionaryRef类),并且通过阈值来决定存储域文件系统还是SQLite,来保证快速存取。
其中对于数据大小的磁盘缓存选择:
- sqlite: 对于小数据(例如 NSNumber)的存取效率明显高于 file。
- file: 对于较大数据(例如高质量图片)的存取效率优于 sqlite。
初始化方法
//.h
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
- (nullable instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER;
//.m
- (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];
}
复制代码
LRU缓存策略
LRU的实现:
- 新数据插入到缓存表;
- 每当缓存命中(即缓存数据被访问),则更新该缓存对应的表中数据行对应的
访问时间
; - 当需要清除缓存时,根据
访问时间
将最旧的条目删除;
YYKVStorage
YYKVStorage 是基于 sqlite 和文件系统的键值存储。通常情况下,我们不应该直接使用这个类。 这个类的实例是 非 线程安全的,你需要确保 只有一个线程可以同时访问该实例。如果你真的需要在多线程中处理大量的数据,应该分割数据 到多个KVStorage 实例(分片)。
/**
存储类型,指示“YYKVStorageItem.value”存储在哪里。
@discussion
通常,将数据写入 sqlite 比外部文件更快,但是
读取性能取决于数据大小。在我的测试(环境 iPhone 6 64G),
当数据较大(超过 20KB)时从外部文件读取数据比 sqlite 更快。
*/
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0, // value 以文件的形式存储于文件系统
YYKVStorageTypeSQLite = 1, // value 以二进制形式存储于 sqlite
YYKVStorageTypeMixed = 2, // value 将根据你的选择基于上面两种形式混合存储
};
@interface YYKVStorage : NSObject
#pragma mark - Attribute
@property (nonatomic, readonly) NSString *path; ///< The path of this storage.
@property (nonatomic, readonly) YYKVStorageType type; ///< The type of this storage.
@property (nonatomic) BOOL errorLogsEnabled; ///< Set `YES` to enable error logs for debug.
#pragma mark - Initializer
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;
#pragma mark - Save Items
- (BOOL)saveItem:(YYKVStorageItem *)item;
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
- (BOOL)saveItemWithKey:(NSString *)key
value:(NSData *)value
filename:(nullable NSString *)filename
extendedData:(nullable NSData *)extendedData;
#pragma mark - Remove Items
- (BOOL)removeItemForKey:(NSString *)key;
- (BOOL)removeItemsLargerThanSize:(int)size;
.....
- (BOOL)removeAllItems;
- (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
#pragma mark - Get Items
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
- (nullable NSData *)getItemValueForKey:(NSString *)key;
.....
#pragma mark - Get Storage Status
- (BOOL)itemExistsForKey:(NSString *)key;
- (int)getItemsCount;
- (int)getItemsSize;
@end
复制代码
YYKVStorageItem
这个类对应就是存储在SQLite表中的数据:
说明 | YYKVStorageItem | SQLite |
---|---|---|
key | key | key |
value,以二进制数据保存 | value | inline_data |
文件系统存储对应的文件名 | filename | filename |
大小 | size | size |
修改时间(在这里为修改时间) | modTime | modification_time |
访问时间 | accessTime | last_access_time |
扩展数据 | extendedData | extended_data |
存储到文件还是SQLite
YYDiskCache对于是存储到文件系统,还是SQLite,有一个阈值20K,是作者测试出得到的一个阈值。
- 小于该阈值,采用SQLite访问速度更快;
- 大于该阈值,采用文件系统访问速度更快;
该阈值在初始化时指定:
- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}
/**
threshold
0:使用文件存储
NSUIntegerMax 则采用sqlite
*/
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}
.....
}
复制代码
所以,假如不指定阈值或者介于0与NSUIntegerMax,YYKVStorage存储类型均为YYKVStorageTypeMixed。即采用根据阈值来进行混合存储。
但我们通过源码发现,就算YYKVStorage存储类型为YYKVStorageTypeFile,还是会在SQLite存储对应的条目,因为我们需要实现LRU驱逐缓存对象。
YYKVStorage存储类型均为YYKVStorageTypeSQLite,此时将不再在文件系统存储对应的对象。
存 | 读 | 删 | |
---|---|---|---|
YYKVStorageTypeSQLite | * SQLite | * SQLite | * SQLite |
YYKVStorageTypeFile | _ 文件系统 | ||
_ SQLite | * SQLite | _ 文件系统 | |
_ SQLite | |||
YYKVStorageTypeMixed | _ 文件系统 | ||
_ SQLite | * SQLite | _ 文件系统 | |
_ SQLite |
存
/**
存储数据
@param key key
@param value 值
@param filename 存储的文件名
@param extendedData 扩展数据
*/
- (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;
}
//有文件名,直接以默认文件及sqlite两种方式存储
if (filename.length) {
//文件存储失败直接返回
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
//sqlite存储失败,删除本地文件存储,然后返回
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
//文件名为空时,默认按sqlite方式存储,并且检查当前如果不是仅仅以sqlite存储(文件存储)时,需要删除该key对应的文件名
if (_type != YYKVStorageTypeSQLite) {
//如果当前存储方式,不是sqlite,那么需要找出文件名,并且如果找到文件名,删除文件之后存储到sqlite
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
复制代码
读
- (BOOL)removeItemsToFitCount:(int)maxCount {
if (maxCount == INT_MAX) return YES;
if (maxCount <= 0) return [self removeAllItems];
int total = [self _dbGetTotalItemCount];
if (total < 0) return NO;
if (total <= maxCount) return YES;
NSArray *items = nil;
BOOL suc = NO;
do {
int perCount = 16;
items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
for (YYKVStorageItem *item in items) {
if (total > maxCount) {
if (item.filename) {
[self _fileDeleteWithName:item.filename];
}
suc = [self _dbDeleteItemWithKey:item.key];
total--;
} else {
break;
}
if (!suc) break;
}
} while (total > maxCount && items.count > 0 && suc);
if (suc) [self _dbCheckpoint];
return suc;
}
复制代码
删
/**
该方法能获取到key对应的所有数据
包括value(来源于sqlite,当存在文件时,从文件读取)
*/
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
if (key.length == 0) return nil;
//excludeInlineData 获取是否包含inline data,就是key对应的value数据
//NO,即包含value数据
YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
if (item) {
//更新访问时间,用于删除时判断是否最近访问的数据
[self _dbUpdateAccessTimeWithKey:key];
if (item.filename) {
//如果也有文件存储了,那么从文件系统读取value
item.value = [self _fileReadWithName:item.filename];
if (!item.value) {
//若是item的数据为空,直接删除sqlite的数据
[self _dbDeleteItemWithKey:key];
item = nil;
}
}
}
return item;
}
复制代码
NSMapTable
NSMapTable 是类似于字典的集合,但具有更广泛的可用内存语义。NSMapTable 是 iOS6 之后引入的类,它基于 NSDictionary 建模,但是具有以下差异:
- 键/值可以选择 “weakly” 持有,以便于在回收其中一个对象时删除对应条目。
- 它可以包含任意指针(其内容不被约束为对象)。
- 您可以将 NSMapTable 实例配置为对任意指针进行操作,而不仅仅是对象。
Note: 配置映射表时,请注意,只有 NSMapTableOptions 中列出的选项才能保证其余的 API 能够正常工作,包括复制,归档和快速枚举。 虽然其他 NSPointerFunctions 选项用于某些配置,例如持有任意指针,但并不是所有选项的组合都有效。使用某些组合,NSMapTableOptions 可能无法正常工作,甚至可能无法正确初始化。
更多信息详见 NSMapTable 官方文档。
每一个YYDiskCache
实例存储于一个全局的NSMapTabel
类的_globalInstances
实例中,以达到快速访问:
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
self = [super init];
if (!self) return nil;
//假如,已经在该path有cache对象,直接返回
YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
if (globalCache) return globalCache;
//否则,创建一个cache对象,
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
if (!kv) return nil;
_YYDiskCacheSetGlobal(self);
return self;
}
/// weak reference for all instances
// 存储所有弱引用的Cache对象,并通过key来获取
static NSMapTable *_globalInstances;
static dispatch_semaphore_t _globalInstancesLock;
static void _YYDiskCacheInitGlobal() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_globalInstancesLock = dispatch_semaphore_create(1);
//maptable 就是一个 dictionary
// 区别是:dictionary 的key 是 copy语义,value是 retain语义
// maptable key、value可以自定义
_globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
});
}
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;
}
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);
}
复制代码
线程安全
YYKVStore本身不支持线程安全,所以不推荐直接使用。
但是YYDiskCache支持线程安全,其通过信号量dispatch_semaphore
来实现互斥锁。
dispatch_semaphore
当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
磁盘缓存中两处使用了信号量的互斥锁:
一处上面说的用_globalInstances
中获取存储的YYDiskCache
的实例对象,需要进行互斥读写:
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
id cache = [_globalInstances objectForKey:path];
// or [_globalInstances setObject:cache forKey:cache.path];
dispatch_semaphore_signal(_globalInstancesLock);
复制代码
另外一处,是在_kv
所有对item
进行操作(包括读、写、删、查),并且 定义的两个宏来方便使用:
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
复制代码
架构
分层
YYCache有优秀的分层设计。
性能
其实性能这个东西是隐而不见的,又是到处可见的(笑)。它从我们最开始设计一个缓存架构时就被带入,一直到我们具体的实现细节中慢慢成形,最后成为了我们设计出来的缓存优秀与否的决定性因素。
上文中剖析了太多 YYCache 中对于性能提升的实现细节:
- 异步释放缓存对象
- 锁的选择
- 使用 NSMapTable 单例管理的 YYDiskCache
- YYKVStorage 中的
_dbStmtCache
- 使用 CoreFoundation 来换取微乎其微的性能提升
锁的选择
- 磁盘缓存采用
dispatch_semaphore
、内存缓存采用pthread_mutex
dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
- 不采用OSSpinLock
OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
不再安全的 OSSpinLock
自从 OSSpinLock 被公布不安全,第三方库纷纷用pthread_mutex 替换 OSSpinLock。
ibireme 在确认 OSSpinLock
不再安全之后为了寻找替代方案做的简单性能测试,对比了一下几种能够替代 OSSpinLock
锁的性能。在 不再安全的 OSSpinLock 文末的评论中,我找到了作者使用 pthread_mutex
的原因。
ibireme: 苹果员工说 libobjc 里
spinlock
是用了一些私有方法 (mach_thread_switch
),贡献出了高线程的优先来避免优先级反转的问题,但是我翻了下 libdispatch 的源码倒是没发现相关逻辑,也可能是我忽略了什么。在我的一些测试中,OSSpinLock
和dispatch_semaphore
都不会产生特别明显的死锁,所以我也无法确定用dispatch_semaphore
代替OSSpinLock
是否正确。能够肯定的是,用pthread_mutex
是安全的。