
区别于NSDictionary, YYMemoryCache对key是retain而不是copy操作(YYMemoryCache的key可以是任何对象的原因, 内部是使用CFMutableDictionaryRef实现的), 提供的API和NSCache类似, 并且YYMemoryCache的所有方法都是安全的.
YYMemoryCache相比NSCache的特点:
- 使用LRU(least-recently-used)淘汰算法(NSCache是清除所有的缓存信息)
- 使用cost count age三个维度清除缓存(NSCache值提供了cost count两个维度)
- 自动清除缓存(当收到内存警告时 会在后台清除缓存)
YYMemoryCache中使用一个双向链表和字典进行增删改查操作的:
- 当需要增加新的缓存时, 会生成一个结点并设置为头结点
- 当需要获取一个缓存时, 从YYLinkedMap->_dic中通过key获取到对应的node结点, 并将node结点设置为头结点
- 当需要删除一个缓存时, 从YYLinkedMap->_dic中通过key删除到对应的node结点, 并将node结点从双向链表中删除
- 当需要按需清除缓存时, 从尾结点开始清除
一. YYLinkedMapNode
YYLinkedMapNode可以理解为链表中的结点. YYMemoryCache中是将传入的value封装成node存储到缓存中的.
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key; // key
id _value; // value
NSUInteger _cost; // 结点的开销, 在内存中占用的字节数
NSTimeInterval _time; // 操作结点的时间
}
@end
@implementation _YYLinkedMapNode
@end
需要注意的是Node并不持有对_prev和_next的强引用, _prev和_next执行的内容是由_dic强引用
二. YYLinkedMap
YYLinkedMap可以理解为是一个双向链表, 方便操作头结点和尾结点.
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly key: key, value: Node结点
NSUInteger _totalCost; // 总开销
NSUInteger _totalCount; // 缓存数量
_YYLinkedMapNode *_head; // MRU, do not change it directly 头结点
_YYLinkedMapNode *_tail; // LRU, do not change it directly 尾结点
BOOL _releaseOnMainThread; // 是否在主线程release结点
BOOL _releaseAsynchronously; // 是否异步release结点
}
思考: 问什么YYLinkedMap中要使用一个字典呢?
使用字典是为了使双向链表的查询某个结点的操作时间复杂度为O(1), 如果是双向链表获取某一个结点的话需要遍历双向链表, 时间复杂度为O(n)
YYLinkedMap中使用CFMutableDictionaryRef创建字典, 而不是使用NSDictionary创建字典的原因:
因为NSDictionary的key需要遵循NSCoding协议, 而CFMutableDictionaryRef则不需要, 另外CFMutableDictionaryRef更加靠近底层, 效率更高, 但是创建的_dic的内存需要我们自己手动回收.
YYLinkedMap的接口:
// 在链表头部插入结点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
// 将结点移动到链表头部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
// 移除结点
- (void)removeNode:(_YYLinkedMapNode *)node;
// 移除尾结点
- (_YYLinkedMapNode *)removeTailNode;
// 清空所有结点
- (void)removeAll;
YYLinkedMap的操作主要是对双向链表进行的插入 删除操作, 下图中虚线为弱引用, 实线为强引用

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
// 将结点node保存在_dic中
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
// 根据结点的cost开销修改map记录的总开销数
_totalCost += node->_cost;
// 记录缓存个数
_totalCount++;
if (_head) {
// 将结点node设置为头结点
node->_next = _head;
_head->_prev = node;
_head = node;
} else {
// 如果头结点为nil, 说明链表为空, 添加的结点node就是头结点 并且还是尾结点
_head = _tail = node;
}
}
移动结点:
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
// 如果只有一个结点, 说明不需要移动
if (_head == node) return;
if (_tail == node) {
// 如果node是尾结点, 重新设置尾结点
_tail = node->_prev;
_tail->_next = nil;
} else {
// node既不是头结点又不是尾结点, 相当于删除结点node
node->_next->_prev = node->_prev;
node->_prev->_next = node->_next;
}
// 将node设置为头结点
node->_next = _head;
node->_prev = nil;
_head->_prev = node;
_head = node;
}
删除结点:
- (void)removeNode:(_YYLinkedMapNode *)node {
// 将结点node从字典_dic中移除, 注意这是node的引用计数减1
CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
// 修改总开销
_totalCost -= node->_cost;
// 缓存数量减1
_totalCount--;
// 修改node结点的下一个结点prev指向
if (node->_next) node->_next->_prev = node->_prev;
// 修改node结点的上一个结点的next指向
if (node->_prev) node->_prev->_next = node->_next;
// 如果node是头结点, 将头结点设置为node的下一个结点
if (_head == node) _head = node->_next;
// 如果node是尾结点, 将尾结点设置为node的上一个结点
if (_tail == node) _tail = node->_prev;
}
注意这里的结点操作执行的顺序, 考虑node是头结点或者尾结点 以及为中间结点的处理情况. 移除尾结点:
- (_YYLinkedMapNode *)removeTailNode {
if (!_tail) return nil;
// 获取尾结点
_YYLinkedMapNode *tail = _tail;
// 将尾结点从字典中移除, 因为字典有强引用
CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key));
_totalCost -= _tail->_cost;
_totalCount--;
if (_head == _tail) {
// 如果头结点=尾结点, 移除之后链表为空
_head = _tail = nil;
} else {
// 否者重置尾结点
_tail = _tail->_prev;
_tail->_next = nil;
}
return tail;
}
清空数据:
- (void)removeAll {
// 清空信息
_totalCost = 0;
_totalCount = 0;
_head = nil;
_tail = nil;
if (CFDictionaryGetCount(_dic) > 0) {
// 相当于对_dic进行了一次mutableCopy, 由于_dic是不可变, 所以holder和_dic执行了同一块内存空间(堆空间)
CFMutableDictionaryRef holder = _dic;
// 重新在堆空间申请内存, _dic执行新分配的内存(之前堆空间的内存地址保存在holder中)
_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
// 如果需要异步执行, 不阻塞当前线程
if (_releaseAsynchronously) {
dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
CFRelease(holder); // hold and release in specified queue
});
} else if (_releaseOnMainThread && !pthread_main_np()) {
// 主线程执行
dispatch_async(dispatch_get_main_queue(), ^{
CFRelease(holder); // hold and release in specified queue
});
} else {
// 主线程执行
CFRelease(holder);
}
}
}
清空数据这里作者的操作很巧妙, 利用holder引用dic, 重新给dic申请堆空间, 在指定的队列中实现对holder的release操作.
三. YYMemoryCache
YYMemoryCache是对外提供的类, 提供了一些配置属性和外界访问的方法.
1. 属性和方法
@interface YYMemoryCache : NSObject
// 缓存名称
@property (nullable, copy) NSString *name;
// 缓存的总数量
@property (readonly) NSUInteger totalCount;
// 缓存的总开销
@property (readonly) NSUInteger totalCost;
// 缓存的数量限制, 默认值是NSUIntegerMax表示不做限制
@property NSUInteger countLimit;
// 缓存的开销限制, 默认值是NSUIntegerMax表示不做限制, 例如设置缓存总开销为200k, 那么当总开销大于200k时就会在后台自动清理
@property NSUInteger costLimit;
// 缓存的时间限制, 默认是DBL_MAX表示不限制
@property NSTimeInterval ageLimit;
// 自动清理时间设置, 默认值是5s, 也就是5秒钟会自动检查清理缓存
@property NSTimeInterval autoTrimInterval;
// 当收到内存警告时, 是否清除所有缓存, 默认是YES
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;
// 当App进入后台时, 是否清除所有缓存, 默认是YES
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
// 收到内存警告时执行的block
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
// 进入后台时执行的block
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
// 当需要移除某个value时, release操作是否在主线程执行, 默认为NO
@property BOOL releaseOnMainThread;
// 当需要移除某个value时, release造作是否异步执行, 默认为YES
@property BOOL releaseAsynchronously;
// 缓存中是否存在key
- (BOOL)containsObjectForKey:(id)key;
// 从缓存中获取和key相关联的value
- (nullable id)objectForKey:(id)key;
// 缓存key-value, 如果value=nil, 表示移除key的缓存
- (void)setObject:(nullable id)object forKey:(id)key;
// 缓存key-value, 指定开销
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
// 从缓存中移除key
- (void)removeObjectForKey:(id)key;
// 清空缓存
- (void)removeAllObjects;
// 将缓存修剪到指定的数量, 从链表的尾结点移除
- (void)trimToCount:(NSUInteger)count
// 将缓存修剪到指定的开销, 从链表的尾结点移除
- (void)trimToCost:(NSUInteger)cost;
// 将缓存修剪到指定的时间, 大于这个时间的数据都会被清除
- (void)trimToAge:(NSTimeInterval)age;
@end
2. 初始化配置
在YYMemoryCahce的初始化方法中, 对一些属性进行了默认参数配置
@implementation YYMemoryCache {
pthread_mutex_t _lock; // 互斥锁
_YYLinkedMap *_lru; // least recent used
dispatch_queue_t _queue; // 串行队列
}
- (instancetype)init {
self = super.init;
// 初始化锁
pthread_mutex_init(&_lock, NULL);
// 初始化双向链表
_lru = [_YYLinkedMap new];
// 串行队列, 用于执行修剪操作, 为什么是串行队列?
_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_autoTrimInterval = 5.0;
_shouldRemoveAllObjectsOnMemoryWarning = YES;
_shouldRemoveAllObjectsWhenEnteringBackground = YES;
// 注册App的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
// 自动清除功能
[self _trimRecursively];
return self;
}
3. 自动清理缓存
在init方法中调用了_trimRecursively方法.
- (void)_trimRecursively {
// 使用weakself, block不会对self进行强引用
__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修饰, 防止self被提前销毁
__strong typeof(_self) self = _self;
if (!self) return;
[self _trimInBackground];
[self _trimRecursively];
});
}
- (void)_trimInBackground {
// 为什么是串行队列? 串行队列中的任务会等待上个任务执行完成以后才执行, 是想按照cost->count->age的顺序去清除缓存
dispatch_async(_queue, ^{
[self _trimToCost:self->_costLimit];
[self _trimToCount:self->_countLimit];
[self _trimToAge:self->_ageLimit];
});
}
_trimRecursively方法中使用一个dispatch_after进行延时操作, 延时时间就是属性autoTrimInterval, 通过延时递归调用_trimRecursively方法实现自动清理缓存的功能.
在_trimRecursively中为什么使用__weak和__strong去修饰self呢?
当不使用__weak和__strong修饰self的时候, demo如下:
@interface MyObject : NSObject
@end
@implementation MyObject
- (void)_trimRecursively {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSLog(@"1-----");
if (!self) return; // block会捕获self, copy, self引用计数加一.
[self _trimRecursively]; // 因为这里是异步调用, 不会阻塞当前线程(递归调用方法中又会重复对self进行 copy和release操作)
NSLog(@"2-----"); // 执行完block以后, block释放对self的引用, release, self引用计数减一.
});
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
MyObject *obj = [[MyObject alloc] init];
[obj _trimRecursively];
}
@end
当viewDidLoad执行完成以后, obj就应该被释放, 但是在_trimRecursively的block中一直存在对obj的强引用, 所以就造成了obj永远不会被释放.
4. 手动清理缓存
_trimToCost _trimToCount _trimToAge三种清理缓存的方法处理是一样的, 只是清除缓存的依据不一样, 所以这里只分析了一种情况.
// static修饰可以在多个文件中定义
// inline内联函数, 提高效率, 在调用的时候是在调用出直接替换, 而普通的函数需要开辟栈空间(push pop操作)
static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
}
- (void)_trimToCost:(NSUInteger)costLimit {
BOOL finish = NO;
// 避免多线程访问出现数据错乱, 所以选择加锁
pthread_mutex_lock(&_lock);
if (costLimit == 0) {
// 如果内存开销设置为0, 表示清空缓存
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCost <= costLimit) {
// 如果当前缓存的开销小于指定限制开销, 不需要清理
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;
// 创建一个数组, 数组中存放的是结点, 为了在指定的线程 异步或者同步release
NSMutableArray *holder = [NSMutableArray new];
// 这里的时间复杂度是O(n)???
while (!finish) {
// 尝试加锁, 如果成功就为0
if (pthread_mutex_trylock(&_lock) == 0) {
// 将需要移除的结点添加到数组中
if (_lru->_totalCost > costLimit) {
// 这里node虽然从dic字典和双向链表中移除了, 但是又加入了数组中, 所以这里node并不会被销毁
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
// 为了防止争夺资源, sleep10ms
usleep(10 * 1000); //10 ms
}
}
// holder中保存了需要release的node结点
if (holder.count) {
// 根据配置选择是否在主线程中release结点资源
// YYMemoryCacheGetReleaseQueue()是内联函数, 创建的全局并发队列
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
// 在队列中移除
dispatch_async(queue, ^{
// 这里我的理解是 block会对holder强引用, 当block执行完成以后holder的引用计数减1
[holder count]; // release in queue
});
}
// 因为block中对holder有强引用, 所以出了}以后, holder的引用计数减1, 不会被销毁, 而是在block中销毁(实现在指定的队列中销毁).
}
5. 属性的setter方法
YYMemoryCache中提供了一些配置属性, 通过重写setter方法, 实现对YYLinkedMap的配置
- (NSUInteger)totalCount {
pthread_mutex_lock(&_lock);
NSUInteger count = _lru->_totalCount;
pthread_mutex_unlock(&_lock);
return count;
}
- (NSUInteger)totalCost {
pthread_mutex_lock(&_lock);
NSUInteger totalCost = _lru->_totalCost;
pthread_mutex_unlock(&_lock);
return totalCost;
}
- (BOOL)releaseOnMainThread {
pthread_mutex_lock(&_lock);
BOOL releaseOnMainThread = _lru->_releaseOnMainThread;
pthread_mutex_unlock(&_lock);
return releaseOnMainThread;
}
- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread {
pthread_mutex_lock(&_lock);
_lru->_releaseOnMainThread = releaseOnMainThread;
pthread_mutex_unlock(&_lock);
}
- (BOOL)releaseAsynchronously {
pthread_mutex_lock(&_lock);
BOOL releaseAsynchronously = _lru->_releaseAsynchronously;
pthread_mutex_unlock(&_lock);
return releaseAsynchronously;
}
6. 增删改查操作
查找:
- (BOOL)containsObjectForKey:(id)key {
if (!key) return NO;
// 因为需要操作map中的数据, 加锁
pthread_mutex_lock(&_lock);
BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
pthread_mutex_unlock(&_lock);
return contains;
}
- (id)objectForKey:(id)key {
if (!key) return nil;
pthread_mutex_lock(&_lock);
// 先从字典中获取node
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
// 因为操作了node, 所以需要将node的时间修改成当前时间
node->_time = CACurrentMediaTime();
// 操作了结点, 将结点设置为头结点.
[_lru bringNodeToHead:node];
}
pthread_mutex_unlock(&_lock);
return node ? node->_value : nil;
}
增加:
- (void)setObject:(id)object forKey:(id)key {
[self setObject:object forKey:key withCost:0];
}
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
if (!key) return;
// 如果object为nil, 说明是移除key
if (!object) {
[self removeObjectForKey:key];
return;
}
pthread_mutex_lock(&_lock);
// 将node存储在字典中
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
// 获取当前时间, 用于设置node的操作时间
NSTimeInterval now = CACurrentMediaTime();
if (node) {
// 如果node已经存在缓存中, 需要将node的time修改, 并且将node添加到表头
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now;
node->_value = object;
[_lru bringNodeToHead:node];
} else {
// 如果node不存在, 需要创建node, 并且添加到表头
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
[_lru insertNodeAtHead:node];
}
// 添加完成以后, 因为_totalCost增加, 需要检查总开销是否超过了设定的标准_costLimit
if (_lru->_totalCost > _costLimit) {
// 在串行队列中异步清除缓存
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
// 添加完成以后, 因为_totalCount增加, 需要检查_totalCount是否大于_countLimit
if (_lru->_totalCount > _countLimit) {
// 因为每次只增加一个node, 所以只需要移除最后一个尾结点即可
_YYLinkedMapNode *node = [_lru removeTailNode];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[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) {
[_lru removeNode:node];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[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)removeAllObjects {
pthread_mutex_lock(&_lock);
[_lru removeAll];
pthread_mutex_unlock(&_lock);
}
7. 其他
- (NSString *)description {
if (_name) return [NSString stringWithFormat:@"<%@: %p> (%@)", self.class, self, _name];
else return [NSString stringWithFormat:@"<%@: %p>", self.class, self];
}
重写description方法, 当log YYMemoryCache的对象时就会调用这个方法, log相关信息.
四. 思考
1. MRU LRU 淘汰算法?
LRU(least-recently-used): 最近最少使用, 是一种淘汰算法, 如果最近模块数据被访问, 那么将来这块数据被访问的概率也很大. 最久没有被访问的数据, 将来被访问的概率会很小. 将最久没有使用的淘汰掉, 需要使用一个time字段, 记录每次访问的时间, 当需要淘汰的时候, 就选择time最大的淘汰.
MRU(most-recently-used): 最近最常使用, 和LRU相反.
2. YYMemoryCache是如何保证线程安全的?
YYMemoryCache需要操作资源时加锁. Pthread_mutex锁.
YYMemoryCache中对清除缓存的操作 访问_YYLinkedMap中的数据 以及getter setter方法都加锁处理, 避免多线程访问的时候出现数据错乱.
3. YYMemoryCache是从哪几个方面提高性能的?
- 选择合适的数据结构, 减少时间复杂度, YYMemoryCache中是使用双向链表+字典, 双向链表方便操作头结点和尾结点, 方便查找和修改中间结点的上一个和下一个节点, 操作的时间复杂度为O(1), 而双向链表的查找复杂度为O(n). 字典的查找复杂度为O(1), YYMemoryCache使用双向链表+字典的数据结构, 增删改查操作的时间复杂度都为O(1).
- 清除缓存的操作异步执行, release对象异步执行
4. dispatch_semaphore和Pthread_mutex锁, 性能对比 如何选择?
// dispatch_semaphore是信号量, 当信号量为1时也可以当做锁来使用, 当信号量小于1时就会等待
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
...
dispatch_semaphore_signal(lock);
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
pthread_mutex_lock(&lock);
...
pthread_mutex_unlock(&lock);
当没有等待的情况下, dispatch_semaphore的性能要比pthread_mutext高, 但是一旦出现等待的情况, 性能就会下降很多.