New file |
| | |
| | | // |
| | | // YYMemoryCache.m |
| | | // YYCache <https://github.com/ibireme/YYCache> |
| | | // |
| | | // Created by ibireme on 15/2/7. |
| | | // Copyright (c) 2015 ibireme. |
| | | // |
| | | // This source code is licensed under the MIT-style license found in the |
| | | // LICENSE file in the root directory of this source tree. |
| | | // |
| | | |
| | | #import "YYMemoryCache.h" |
| | | #import <UIKit/UIKit.h> |
| | | #import <CoreFoundation/CoreFoundation.h> |
| | | #import <QuartzCore/QuartzCore.h> |
| | | #import <pthread.h> |
| | | |
| | | |
| | | static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() { |
| | | return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); |
| | | } |
| | | |
| | | /** |
| | | 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 |
| | | |
| | | @implementation _YYLinkedMapNode |
| | | @end |
| | | |
| | | |
| | | /** |
| | | A linked map used by YYMemoryCache. |
| | | It's not thread-safe and does not validate the parameters. |
| | | |
| | | Typically, you should not use this class directly. |
| | | */ |
| | | @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; |
| | | } |
| | | |
| | | /// Insert a node at head and update the total cost. |
| | | /// Node and node.key should not be nil. |
| | | - (void)insertNodeAtHead:(_YYLinkedMapNode *)node; |
| | | |
| | | /// Bring a inner node to header. |
| | | /// Node should already inside the dic. |
| | | - (void)bringNodeToHead:(_YYLinkedMapNode *)node; |
| | | |
| | | /// Remove a inner node and update the total cost. |
| | | /// Node should already inside the dic. |
| | | - (void)removeNode:(_YYLinkedMapNode *)node; |
| | | |
| | | /// Remove tail node if exist. |
| | | - (_YYLinkedMapNode *)removeTailNode; |
| | | |
| | | /// Remove all node in background queue. |
| | | - (void)removeAll; |
| | | |
| | | @end |
| | | |
| | | @implementation _YYLinkedMap |
| | | |
| | | - (instancetype)init { |
| | | self = [super init]; |
| | | _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); |
| | | _releaseOnMainThread = NO; |
| | | _releaseAsynchronously = YES; |
| | | return self; |
| | | } |
| | | |
| | | - (void)dealloc { |
| | | CFRelease(_dic); |
| | | } |
| | | |
| | | - (void)insertNodeAtHead:(_YYLinkedMapNode *)node { |
| | | CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node)); |
| | | _totalCost += node->_cost; |
| | | _totalCount++; |
| | | if (_head) { |
| | | node->_next = _head; |
| | | _head->_prev = node; |
| | | _head = node; |
| | | } else { |
| | | _head = _tail = node; |
| | | } |
| | | } |
| | | |
| | | - (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; |
| | | } |
| | | |
| | | - (void)removeNode:(_YYLinkedMapNode *)node { |
| | | CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key)); |
| | | _totalCost -= node->_cost; |
| | | _totalCount--; |
| | | if (node->_next) node->_next->_prev = node->_prev; |
| | | if (node->_prev) node->_prev->_next = node->_next; |
| | | if (_head == node) _head = node->_next; |
| | | if (_tail == node) _tail = node->_prev; |
| | | } |
| | | |
| | | - (_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) { |
| | | CFMutableDictionaryRef holder = _dic; |
| | | _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); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @end |
| | | |
| | | |
| | | |
| | | @implementation YYMemoryCache { |
| | | pthread_mutex_t _lock; |
| | | _YYLinkedMap *_lru; |
| | | dispatch_queue_t _queue; |
| | | } |
| | | |
| | | - (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]; |
| | | }); |
| | | } |
| | | |
| | | - (void)_trimInBackground { |
| | | dispatch_async(_queue, ^{ |
| | | [self _trimToCost:self->_costLimit]; |
| | | [self _trimToCount:self->_countLimit]; |
| | | [self _trimToAge:self->_ageLimit]; |
| | | }); |
| | | } |
| | | |
| | | - (void)_trimToCost:(NSUInteger)costLimit { |
| | | BOOL finish = NO; |
| | | pthread_mutex_lock(&_lock); |
| | | if (costLimit == 0) { |
| | | [_lru removeAll]; |
| | | finish = YES; |
| | | } else if (_lru->_totalCost <= costLimit) { |
| | | finish = YES; |
| | | } |
| | | pthread_mutex_unlock(&_lock); |
| | | if (finish) return; |
| | | |
| | | NSMutableArray *holder = [NSMutableArray new]; |
| | | while (!finish) { |
| | | if (pthread_mutex_trylock(&_lock) == 0) { |
| | | if (_lru->_totalCost > costLimit) { |
| | | _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]; // release in queue |
| | | }); |
| | | } |
| | | } |
| | | |
| | | - (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) { |
| | | _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]; // release in queue |
| | | }); |
| | | } |
| | | } |
| | | |
| | | - (void)_trimToAge:(NSTimeInterval)ageLimit { |
| | | BOOL finish = NO; |
| | | NSTimeInterval now = CACurrentMediaTime(); |
| | | pthread_mutex_lock(&_lock); |
| | | if (ageLimit <= 0) { |
| | | [_lru removeAll]; |
| | | finish = YES; |
| | | } else if (!_lru->_tail || (now - _lru->_tail->_time) <= ageLimit) { |
| | | finish = YES; |
| | | } |
| | | pthread_mutex_unlock(&_lock); |
| | | if (finish) return; |
| | | |
| | | NSMutableArray *holder = [NSMutableArray new]; |
| | | while (!finish) { |
| | | if (pthread_mutex_trylock(&_lock) == 0) { |
| | | if (_lru->_tail && (now - _lru->_tail->_time) > ageLimit) { |
| | | _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]; // release in queue |
| | | }); |
| | | } |
| | | } |
| | | |
| | | - (void)_appDidReceiveMemoryWarningNotification { |
| | | if (self.didReceiveMemoryWarningBlock) { |
| | | self.didReceiveMemoryWarningBlock(self); |
| | | } |
| | | if (self.shouldRemoveAllObjectsOnMemoryWarning) { |
| | | [self removeAllObjects]; |
| | | } |
| | | } |
| | | |
| | | - (void)_appDidEnterBackgroundNotification { |
| | | if (self.didEnterBackgroundBlock) { |
| | | self.didEnterBackgroundBlock(self); |
| | | } |
| | | if (self.shouldRemoveAllObjectsWhenEnteringBackground) { |
| | | [self removeAllObjects]; |
| | | } |
| | | } |
| | | |
| | | #pragma mark - public |
| | | |
| | | - (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; |
| | | |
| | | [[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; |
| | | } |
| | | |
| | | - (void)dealloc { |
| | | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; |
| | | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; |
| | | [_lru removeAll]; |
| | | pthread_mutex_destroy(&_lock); |
| | | } |
| | | |
| | | - (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; |
| | | } |
| | | |
| | | - (void)setReleaseAsynchronously:(BOOL)releaseAsynchronously { |
| | | pthread_mutex_lock(&_lock); |
| | | _lru->_releaseAsynchronously = releaseAsynchronously; |
| | | pthread_mutex_unlock(&_lock); |
| | | } |
| | | |
| | | - (BOOL)containsObjectForKey:(id)key { |
| | | if (!key) return NO; |
| | | 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); |
| | | _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); |
| | | if (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; |
| | | if (!object) { |
| | | [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; |
| | | _lru->_totalCost += 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 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); |
| | | _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); |
| | | } |
| | | |
| | | - (void)trimToCount:(NSUInteger)count { |
| | | if (count == 0) { |
| | | [self removeAllObjects]; |
| | | return; |
| | | } |
| | | [self _trimToCount:count]; |
| | | } |
| | | |
| | | - (void)trimToCost:(NSUInteger)cost { |
| | | [self _trimToCost:cost]; |
| | | } |
| | | |
| | | - (void)trimToAge:(NSTimeInterval)age { |
| | | [self _trimToAge:age]; |
| | | } |
| | | |
| | | - (NSString *)description { |
| | | if (_name) return [NSString stringWithFormat:@"<%@: %p> (%@)", self.class, self, _name]; |
| | | else return [NSString stringWithFormat:@"<%@: %p>", self.class, self]; |
| | | } |
| | | |
| | | @end |