YYCache是OC用于缓存的第三方框架。
- YYCache:同时实现内存缓存和磁盘缓存,且是线程安全的。
- YYDiskCache:实现磁盘缓存,所有的API是线程安卓的,内部也采用了LRU淘汰算法,主要是SQLite和文件存储两种方式。
- YYKVStorage:实现磁盘缓存,不推荐直接使用此类,该类不是线程安全的。
- YYMemoryCache:实现内存缓存,所有的API是线程安全的,也是采用了LRU淘汰算法来提高性能。
LRU淘汰算法:
LRU(Least recently used,最近最少使用)算法,根据访问的历史记录来对数据进行淘汰。
- 有新数据加入时添加到链表的头部。
- 每当缓存命中(缓存数据被访问),则将数据移动到链表头部。
- 每当链表满的时候,将链表尾部的数据丢弃。
在YYMemoryCache中使用双向链表和NSDictionary实现了LRU淘汰算法。
线程安全-锁:
YYCache使用到了两种锁
- OSSpinLock:自旋锁
- dispatch_semaphore:信号量,当信号量为1时充当锁进行使用
内存缓存使用的是pthread_mutex,由于pthread_mutex相当于do while忙等,等待时会消耗大量的CPU资源。
@interface _YYLinkedMapNode : NSObject {@package__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic,指向上一个节点__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic,指向下一个节点id _key;// 缓存的keyid _value;// 缓存的对象NSUInteger _cost;// 内存消耗NSTimeInterval _time;// 缓存时间
}
@end
_YYLinkedMapNode:链表的节点
- _prev、_next:分别表示指向上一个节点、下一个节点
- _key:缓存的key
- _value:缓存对象
- _cost:内存消耗
- _time:缓存时间
@interface _YYLinkedMap : NSObject {@packageCFMutableDictionaryRef _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;// 是否在主线程释放_YYLinkedMapNodeBOOL _releaseAsynchronously;// 是否异步释放_YYLinkedMapNode
}
_YYLinkedMap:链表
- _dic:用来保存节点
- _totalCost:总缓存开销
- _head、_tail:头节点、尾节点
- _releaseOnMainThread:是否在主线程释放_YYLinkedMapNode
- _releaseAsynchronously:是否异步释放_YYLinkedMapNode
- 插入节点到头部(- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;)
- 将某个节点移动到头部(- (void)bringNodeToHead:(_YYLinkedMapNode *)node;)
- 移除特定节点(- (void)removeNode:(_YYLinkedMapNode *)node;)
- 移除尾部节点(- (_YYLinkedMapNode *)removeTailNode;)
- 移除所有节点(- (void)removeAll;)
移除所有节点源码:
- (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);}}
}
通过双向链表来对数据进行操作,以及使用NSDictionary实现了LRU淘汰算法。时间复杂度O(1),5种操作基本是都是对节点的基本操作。
YYMemoryCache
这边介绍两个主要操作:添加缓存,查找缓存
添加缓存
-(void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost
{if(!key)return;if(!object){[self removeObjectForKey:key];// 缓存对象为nil时,直接移除return;}pthread_mutex_lock(&_lock);// 为了保证数据安全,数据操作前加锁_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic,(__briddge 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->_totalCount > _countLimit){// 判断当前的缓存进行是否超出了设定值,若超出则进行整理dispatch_async(_queue,^{[self trimToCost:_costLimit];});}if(_lru->_totalCost > _costLimit)// 每次添加数据仅有一个,数量上超出时,直接移除尾部那个object{_YYLinkedMapNode *node = [_lru removeTailNode];//线程操作,异步释放if(_lru->_releaseAsynchronousely){dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue : YYMemoryCacgeGetReleaseQueue();dispatch_async(queue,^{[node class];//通过node来对其进行持有,以至于不会在方法调用结束的时候被销毁});}else if(_lru->_releaseOnMainThread && !pthread_main_np){dispatch_async(dispatch_get_main_queue(),^{[node class];//通过node来对其进行持有,以至于不会在方法调用结束的时候被销毁});}}pthread_mutex_unlock(&_lock);// 操作结束,解锁
}
获取缓存
// 从memory 中取数据时,根据LRU原则,将最新取出的obejct放在栈头
-(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;
}
YYKVStore
该文件主要以两种方式来实现磁盘缓存:SQLite、File,使用两种方式混合进行数据存储主要是为了提高读写效率。写入数据时,SQLite要比文件的方式更快;读取数据的速度取决于文件的大小。据测试,在iphone6中,当文件的大小超过20kb时,File要比SQLite快的多。所以大文件存储时建议采用File的方式,小文件更适合用SQLite.
Save:
- (BOOL)saveItemWithKey:(NSString *)keyvalue:(NSData *)valuefilename:(nullable NSString *)filenameextendedData:(nullable NSData *)extendedData
{//条件不符合if(key.length == 0 || value.length == 0)return NO;if(_type == YYKVStorageTypeFile && filename.length == 0){return NO; }if(filename.length)// filename 存在,sqlite 和filename两种方式进行{// 用文件进行存储if(![self _fileWriteWithName:filename data:value]){return NO;}// 用SQLite进行存储if(![self _dbSaveWithKey:key value:value filename:filename extendedData:extendedData]){// 当使用SQLite存储失败时,删除本地文件存储[self _fileDeleteWithName:filename];return NO;}return YES;}else{// filename不存在时,使用sqliteif(_type != YYKVStorageTypeSDLite){// 根据key删除对应的file文件NSString *filename = [self _dbGetFilenameWithKey:key];if(filename){[self _fileDeleteWithName:filename];}}// sqlite 进行存储return [self _dbSaveWithKey:key value:value filename:nil extendedData:extendedData];}
}
Remove
-(BOOL)removeItemForkey:(NSString *)key
{if(key.length == 0) return NO;switch(_type){case YYKVStorageSQLite:{return [self _dbDeleteItemWithKey:key];// 删除SQLite文件}break;case YYKVStorageTypeFile:case YYKVStorageTypeMixed:{NSString *filename = [self _dbGetFilenameWithKey:key];// 获取filenameif(filename){[self _dbDeleteWithName:filename];// 删除filename对应的file}return [self _dbDeleteItemWithKey:key];// 删除sqlite}break;default:return NO;}
}
Get
- (NSData *)getItemValueForKey:(NSString *)key
{if(key.length == 0) return nil;NSData *value = nil;switch(_type){case YYKVStorageTypeFile:{NSString *filename = [self _dbGetFilenameWithKey:key];if(filename)// 根据filename获取File{value = [self _fileReadWithName:filename];if(!value)// 当value 不存在,用对应的key删除SQLite数据{[self _dbDeleteItemWithKey:key];return nil;}}}break;case YYKVStorageTypeSQLite:// SQLite方式获取{value = [self _dbGetValueWithKey:key];}break;case YYKVStorageTypeMixed:{NSString *filename = [self _dbGetFilenameWithKey:key];if(filename)//filename 存在文件获取,不存在SQLite方式获取{value = [self _fileReadWithName:filename];if(!value){[self _dbDeleteItemWithKey:key];value = nil;}}else{value = [self _dbGetValueWithKey:key];}}break;}if(value){[self _dbUpdateAccessTimeWithKey:key];//更新文件操作时间}return value;
}
File方式主要是用的writeToFile进行存储,SQLite之际是用sqlite3来对文件进行操作。
YYDiskCache
YYDiskCache是对YYKVStorage进行的一次封装,是线程安全的,这边的操作使用的是dispatch_semaphore_signal来确保线程的安全。另外结合LRU淘汰算法,根据文件的大小自动选择存储方式来表达到更好的性能。
- (instancetype)initWithPath:(NSString *)pathinlineThreshold:(NSUInteger)threshold
{self = [super init];if(!self) return nil;//获取缓存的 YYDiskCacheYYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);if(globalCache) return globalCache;//确定存储的方式YYKVStorageType type;if(threshold == 0){type = YYKVStorageTypeFile;}else if(threshold == NSUIntegerMax){type = YYKVStorageTypeSQLite; }else{type = YYKVStorageTypeMixed;}//初始化 YYKVStorageYYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];if(!kv) return nil;//初始化数据_kv = kv;_path = path;_lock = dispatch_semaphore_create(1);_queue = dispatch_queue_create("com.ibireme.cache.disk",DISPATCH_QUEUE_CONCURRENT);_inlineThreshold = threshold;_countLimit = NSUIntegerMax;_costLimit = NSUIntegerMax;_ageLimit = DBL_MAX;_freeDiskSpaceLimit = 0;_autoTrimInterval = 60;// 递归的去整理文件[self _trimRecursively];//对当前对象进行缓存_YYDiskCacheSetGlobal(self);//通知 APP即将被杀死时[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];return self;
}