原文在这里git
缓存是咱们移动端开发必不可少的功能, 目前说起的缓存按照存储形式来分主要分为:github
那缓存的目的是什么呢? 大概分为如下几点:编程
简言之,缓存的目的就是:数组
以空间换时间.缓存
目前 gitHub 上开源了不少缓存框架, 著名的 TMCache, PINCache, YYCache等, 接下来我会逐一分析他们的源码实现, 对比它们的优缺点.安全
TMCache, PINCache, YYCache基本框架结构都相同, 接口 API 相似, 因此只要会使用其中一个框架, 另外两个上手起来很是容易, 可是三个框架的内部实现原理略有不一样.bash
TMMemoryCache
是 TMCache
框架中针对内存缓存的实现, 在系统 NSCache
缓存的基础上增长了不少方法和属性, 好比数量限制、内存总容量限制、缓存存活时间限制、内存警告或应用退到后台时清空缓存等功能. 而且TMMemoryCache
可以同步和异步的对内存数据进行操做,最重要的一点是TMMemoryCache
是线程安全的, 可以确保在多线程状况下数据的安全性.多线程
首先来看一下 TMMemoryCache
提供什么功能, 按照功能来分析它的实现原理:并发
相关 API:框架
// 同步
- (void)setObject:(id)object forKey:(NSString *)key;
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost;
// 异步
- (void)setObject:(id)object forKey:(NSString *)key block:(TMMemoryCacheObjectBlock)block;
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost block:(TMMemoryCacheObjectBlock)block;
复制代码
首先看一下异步存储对象, 由于同步存储里面会调用异步存储操做, 采用 dispatch_semaphore
信号量的方式强制把异步操做转换成同步操做.
内存缓存的核心是建立字典把须要存储的对象按照 key, value的形式存进字典中, 这是一条主线, 而后在主线上分发出许多分支, 好比:缓存时间, 缓存大小, 线程安全等, 都是围绕着这条主线来的. TMMemoryCache 也不例外, 在调用+ (instancetype)sharedCache
方法建立并初始化的时候会建立三个可变字典_dictionary
, _dates
, _costs
,这三个字典分别保存三种键值对:
- | Key | value |
---|---|---|
_dictionary | 存储对象的 key | 存储对象的值 |
_dates | 存储对象的 key | 存储对象时的时间 |
_costs | 存储对象的 key | 存储对象所占内存大小 |
实现数据存储的核心方法:
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost block:(TMMemoryCacheObjectBlock)block {
NSDate *now = [[NSDate alloc] init];
if (!key || !object)
return;
__weak TMMemoryCache *weakSelf = self;
// 0.竞态条件下, 在并发队列中保护写操做
dispatch_barrier_async(_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
if (!strongSelf)
return;
// 1.调用 will add block
if (strongSelf->_willAddObjectBlock)
strongSelf->_willAddObjectBlock(strongSelf, key, object);
// 2.存储 key 对应的数据,时间,缓存大小到相应的字典中
[strongSelf->_dictionary setObject:object forKey:key];
[strongSelf->_dates setObject:now forKey:key];
[strongSelf->_costs setObject:@(cost) forKey:key];
_totalCost += cost;
// 3.调用 did add block
if (strongSelf->_didAddObjectBlock)
strongSelf->_didAddObjectBlock(strongSelf, key, object);
// 4.根据时间排序来清空指定缓存大小的内存
if (strongSelf->_costLimit > 0)
[strongSelf trimToCostByDate:strongSelf->_costLimit block:nil];
// 5.异步回调
if (block) {
__weak TMMemoryCache *weakSelf = strongSelf;
dispatch_async(strongSelf->_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
if (strongSelf)
block(strongSelf, key, object);
});
}
});
}
复制代码
在上面的代码中我标出了核心存储方法作了几件事, 其中最为核心的是保证线程安全的dispatch_barrier_async
方法, 在 GCD 中称之为栅栏
方法, 通常跟并发队列
一块儿用, 在多线程中对同一资源的竞争条件下保护共享资源, 确保在同一时间片断只有一个线程写
资源, 这是不扩展讲 GCD 的相关知识.
dispatch_barrier_async 方法通常都是跟并发队列搭配使用,下面的图解很清晰(
侵删
), 在并发队列中有不少任务(block), 这些block都是按照 FIFO 的顺序执行的, 当要执行用 dispatch_barrier_async 方法提交到并发队列queue的 block 的时候, 该并发队列暂时会'卡住', 等待以前的任务 block 执行完毕, 再执行dispatch_barrier_async 提交的 block, 在此 block 以后提交到并发队列queue的 block 不会被执行,会一直等待 dispatch_barrier_async block 执行完毕后才开始并发执行, 咱们能够看出, 在并发队列遇到 dispatch_barrier_async block 时就处于一直串行队列状态, 等待执行完毕后又开始并发执行.
因为TMMemoryCache中全部的读写操做都是在一个 concurrent queue(并发队列)中, 因此使用dispatch_barrier_async
可以保证写操做的线程安全, 在同一时间只有一个写任务在执行, 其它读写操做都处于等待状态, 这是 TMMemoryCache 保证线程安全的核心, 但也是它最大的毛病, 容易形成性能降低和死锁.![]()
从上面代码中能够看出, 在该方法中把须要存储的数据按照 key-value 的形式存储进了_dictionary
字典中, 其它操做无非就是增长功能的配料,后面会抽丝剥茧的捋清楚, 到此处咱们的任务完成, 知道是怎么存储数据的, 很是简单:
dispatch_barrier_async
方法保证写操做线程安全.根据上文所说, 同步存储中会调用异步存储操做, 来看一下代码:
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost {
if (!object || !key)
return;
// 1.建立信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// 2.异步存数据
[self setObject:object forKey:key withCost:cost block:^(TMMemoryCache *cache, NSString *key, id object) {
// 3.异步存储完毕发送 signal 信号
dispatch_semaphore_signal(semaphore);
}];
// 4.等待异步存储完毕
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
复制代码
从上面代码能够看出,同步的存储数据使用了 GCD 的 dispatch_semaphore_t
信号量, 这是一个很是古老又复杂的线程概念, 有兴趣的话能够看看 <<UNIX 环境高级编程>>
这本经典之做, 由于它的复杂是创建在操做系统的复杂性上的.可是这并不影响咱们使用 dispatch_semaphore_t 信号量. 怎么使用 GCD 的信号量以及原理下面大概描述一下:
信号量在竞态条件下可以保证线程安全,在建立信号量 dispatch_semaphore_create 的时候设置信号量的值, 这个值表示容许多少个线程可同时访问公共资源, 就比如咱们的车位同样, 线程就是咱们的车子,这个信号量就是停车场的管理者, 他知道何时有多少车位, 是否是该把车子放进停车场, 当没有车位或者车位不足时, 这个管理员就会把司机卡在停车场外不许进, 那么被拦住的司机按照 FIFO 的队列排着队, 有足够位置的时候,管理员就方法闸门, 大吼一声: 孩子们去吧. 那么确定有司机等不耐烦了, 就想着等十分钟没有车位就不等了,就能够在 dispatch_semaphore_wait 方法中设置等待时间, 等待超过设置时间就不等待.
那么把上面的场景应用在 dispatch_semaphore_create 信号量中就很容易理解了, 建立信号量并设置最大并发线程数, dispatch_semaphore_wait 设置等待时间,在等待时间未到达或者信号量值没有达到初始值时会一直等待, 调用 dispatch_semaphore_wait 方法会使信号量的值+1, 表示增长一个线程等待处理共用资源, 当 dispatch_semaphore_signal 时会使信号量的值-1, 表示该线程再也不占用共用资源.
根据上面对 dispatch_semaphore_t 信号量的描述可知, 信号量的初始值为0,当前线程执行 dispatch_semaphore_wait 方法就会一直等待, 此时就至关于同步操做, 当在并发队列中异步存储完数据调用dispatch_semaphore_signal 方法, 此时信号量的值变成0,跟初始值同样,当前线程当即结束等待, 同步设置方法执行完毕.
其实同步实现存储数据的方式不少, 主要就是要串行执行写操做采用 dispatch_sync的方式, 可是基于 TMMemoryCache 全部的操做都是在并发队列上的, 因此才采用信号量的方式.
其实只要知道dispatch_barrier_async
, dispatch_semaphore_t
的用法,后面的均可以不用看了, 本身去找源码看看就明白了.
休息一下吧,后面的简单了
有了上面的同步/异步存储的理论, 那么同步/异步获取对象简直易如反掌, 不就是从_dictionary
字典中根据 key 取出对应的 value 值, 在取的过程当中加以线程安全, will/did 之类辅助处理的 block 操做.
- (void)objectForKey:(NSString *)key block:(TMMemoryCacheObjectBlock)block {
NSDate *now = [[NSDate alloc] init];
if (!key || !block)
return;
__weak TMMemoryCache *weakSelf = self;
// 1.异步加载存储数据
dispatch_async(_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
if (!strongSelf)
return;
// 2.根据 key 找到value
id object = [strongSelf->_dictionary objectForKey:key];
if (object) {
__weak TMMemoryCache *weakSelf = strongSelf;
// 3.也用栅栏保护写操做, 保证在写的时候没有线程在访问共享资源
dispatch_barrier_async(strongSelf->_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
if (strongSelf)
// 4.更新数据的最后操做时间(当前时间)
[strongSelf->_dates setObject:now forKey:key];
});
}
// 5.回调
block(strongSelf, key, object);
});
}
复制代码
根据代码中注释可知,除了拿到 key 值对应的 value, 还更新了此数据最后操做时间, 这有什么用呢? 实际上是为了记录数据最后的操做时间, 后面会根据这个最后操做时间来删除数据等一系列根据时间排序的操做.最后一步是回调, 咱们能够看到, TMMemoryCache全部的读写和回调操做都放在同一个并发队列中,这就为之后性能降低和死锁埋下伏笔.
- (id)objectForKey:(NSString *)key {
if (!key)
return nil;
__block id objectForKey = nil;
// 采用信号量强制转化成同步操做
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self objectForKey:key block:^(TMMemoryCache *cache, NSString *key, id object) {
objectForKey = object;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return objectForKey;
}
复制代码
同步获取数据也是经过 dispatch_semaphore_t
信号量的方式,把异步获取数据的操做强制转成同步获取, 跟同步存储数据的原理相同.
删除操做也不例外:
- (void)removeObjectForKey:(NSString *)key block:(TMMemoryCacheObjectBlock)block {
if (!key)
return;
__weak TMMemoryCache *weakSelf = self;
// 1."栅栏"方法,保证线程安全
dispatch_barrier_async(_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
if (!strongSelf)
return;
// 2.根据 key 删除 value
[strongSelf removeObjectAndExecuteBlocksForKey:key];
if (block) {
__weak TMMemoryCache *weakSelf = strongSelf;
// 3.完成后回调
dispatch_async(strongSelf->_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
if (strongSelf)
block(strongSelf, key, nil);
});
}
});
}
// private API
- (void)removeObjectAndExecuteBlocksForKey:(NSString *)key {
id object = [_dictionary objectForKey:key];
NSNumber *cost = [_costs objectForKey:key];
if (_willRemoveObjectBlock)
_willRemoveObjectBlock(self, key, object);
if (cost)
_totalCost -= [cost unsignedIntegerValue];
// 删除全部跟此数据相关的缓存: value, date, cost
[_dictionary removeObjectForKey:key];
[_dates removeObjectForKey:key];
[_costs removeObjectForKey:key];
if (_didRemoveObjectBlock)
_didRemoveObjectBlock(self, key, nil);
}
复制代码
须要注意的是 - (void)removeObjectAndExecuteBlocksForKey
是共用私有方法, 删除跟 key 相关的全部缓存, 后面的删除操做还会用到此方法.
TMMemoryCache 提供costLimit
属性来设置内存缓存使用上限, 这个也是 NSCache 不具有的功能,来看一下跟此属性相关的方法以及实现,代码中有详细解释:
// getter
- (NSUInteger)costLimit {
__block NSUInteger costLimit = 0;
// 要想经过函数返回值传递回去,那么必须同步执行,因此使用dispatch_sync同步获取内存使用上限
dispatch_sync(_queue, ^{
costLimit = _costLimit;
});
return costLimit;
}
// setter
- (void)setCostLimit:(NSUInteger)costLimit {
__weak TMMemoryCache *weakSelf = self;
// "栅栏"方法保护写操做
dispatch_barrier_async(_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
if (!strongSelf)
return;
// 设置内存上限
strongSelf->_costLimit = costLimit;
if (costLimit > 0)
// 根据时间排序来削减内存缓存,以达到设置的内存缓存上限的目的
[strongSelf trimToCostLimitByDate:costLimit];
});
}
- (void)trimToCostLimitByDate:(NSUInteger)limit {
if (_totalCost <= limit)
return;
// 按照时间的升序来排列 key
NSArray *keysSortedByDate = [_dates keysSortedByValueUsingSelector:@selector(compare:)];
// oldest objects first
for (NSString *key in keysSortedByDate) {
[self removeObjectAndExecuteBlocksForKey:key];
if (_totalCost <= limit)
break;
}
}
复制代码
- (void)trimToCostLimitByDate:(NSUInteger)limit
方法的做用:
从这里就会恍然大悟, 以前设置的 _date
数组终于派上用场了,若是须要删除数据则按照时间的前后顺序来删除,也算是一种优先级策略吧.
TMMemoryCache 提供ageLimit
属性来设置缓存过时时间,根据上面costLimit
属性能够猜测一下ageLimit
是怎么实现的,既然是要设置缓存过时时间, 那么我设置缓存过时时间 ageLimit = 10
10秒钟,说明距离当前时间以前的10秒的数据已通过期, 须要删除掉; 再过10秒又要当前时间删除以前10秒存的数据,咱们知道删除只须要找到 key 就行,因此就必须经过_date
字典找到过时的 key, 再删除数据.由此可知须要一个定时器,每过10秒删除一次,完成一个定时任务. 上面只是咱们的猜测,来看看代码是否是这么实现的呢?咱们只需看核心的操做方法
- (void)trimToAgeLimitRecursively {
if (_ageLimit == 0.0)
return;
// 说明距离如今 ageLimit 秒的缓存应该被清除掉了
NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:-_ageLimit];
[self trimMemoryToDate:date];
__weak TMMemoryCache *weakSelf = self;
// 延迟 ageLimit 秒, 又异步的清除缓存
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_ageLimit * NSEC_PER_SEC));
dispatch_after(time, _queue, ^(void){
TMMemoryCache *strongSelf = weakSelf;
if (!strongSelf)
return;
__weak TMMemoryCache *weakSelf = strongSelf;
dispatch_barrier_async(strongSelf->_queue, ^{
TMMemoryCache *strongSelf = weakSelf;
[strongSelf trimToAgeLimitRecursively];
});
});
}
复制代码
上面的代码验证了咱们的猜测,可是在不断的建立定时器,不断的在并行队列中使用dispatch_barrier_async
栅栏方法提交递归 block, 天啦噜...若是设置的 ageLimit 很小,可想而知性能消耗会很是大!
内存警告和退到后台须要监听系统通知,UIApplicationDidReceiveMemoryWarningNotification
和UIApplicationDidEnterBackgroundNotification
, 而后执行清除操做方法removeAllObjects
,只不过在相应的位置执行对应的 will/did 之类的 block 操做.
这两类方法主要是为了更加灵活的使用 TMMemoryCache,指定一个时间或者内存大小,会自动删除时间点以前和大于指定内存大小的数据. 相关 API:
// 清空 date 以前的数据
- (void)trimToDate:(NSDate *)date block:(TMMemoryCacheBlock)block;
// 清空数据,让已使用内存大小为cost
- (void)trimToCost:(NSUInteger)cost block:(TMMemoryCacheBlock)block;
复制代码
删除指定时间点有两点注意:
[NSDate distantPast]
表示最先能表示的时间,说明清空所有数据._date
中的 key 按照升序排序,再遍历排序后的 key 数组,判断跟指定时间的关系,若是比指定时间更早则删除, 即删除指定时间节点以前的数据.- (void)trimMemoryToDate:(NSDate *)trimDate {
// 字典中存放的顺序不是按照顺序存放的, 因此按照必定格式排序, 根据 value 升序的排 key 值顺序, 也就是说根据时间的升序来排 key, 数组中第一个值是最先的时间的值.
NSArray *keysSortedByDate = [_dates keysSortedByValueUsingSelector:@selector(compare:)];
for (NSString *key in keysSortedByDate) { // oldest objects first
NSDate *accessDate = [_dates objectForKey:key];
if (!accessDate)
continue;
// 找出每一个时间的而后跟要删除的时间点进行比较, 若是比删除时间早则删除
if ([accessDate compare:trimDate] == NSOrderedAscending) { // older than trim date
[self removeObjectAndExecuteBlocksForKey:key];
} else {
break;
}
}
}
复制代码
内存缓存是很简单的, 核心就是 key-value 的形式存储数据进字典,再辅助设置内存上限,缓存时间,各种 will/did block 操做, 最重要的是要实现线程安全.
欢迎你们斧正!