最近YYKit在IOS各大論壇討論得火熱,其代碼簡單、高效令人驚嘆。我也湊湊熱鬧,抱著學習為目的的心態解析下ibireme的代碼。這里從比較簡單的YYCache
開始入手,下面是該目錄結構。
YYCache
github地址:https://github.com/ibireme/YYCache
YYCache是用于Objective-C中用于緩存的第三方框架。
YYMemoryCache:內存緩存,并且所有API都是線程安全的。
YYDiskCache:磁盤緩存,主要用SQLite和文件存儲,并且所有API都是線程安全的
LRU算法:Least recently used,最近最少使用
LRU算法
在YYCache的YYMemoryCache
和YYDiskCache
中都采用LRU算法進行快速存取,主要是通過雙向鏈表
和NSMutableDictionry
來實現。下面這張圖很好詮釋了LRU算法。
- 用雙向鏈表來表示堆棧
- 新加入的數據存在棧頂
- 使用緩存的時候,從棧中查找,如果命中,就把數據移到棧頂
- 可以設置棧最大長度,超過長度就把棧尾數據刪除
通過以上的規則,一個簡單的LRU算法就得以實現。
線程安全控制(鎖)
分析YYCache的時候,我發現作者用了很多鎖來保證線程安全。這是值得我學習的地方,因為以前我根本沒有考慮過線程問題。
在這里YYCache主要用了2種鎖:pthread_mutex
和dispatch_semaphore
,下面是作者自己的分析:
OSSpinLock 自旋鎖,性能最高的鎖。原理很簡單,就是一直 do while 忙等。它的缺點是當等待時會消耗大量 CPU 資源,所以它不適用于較長時間的任務。對于內存緩存的存取來說,它非常合適。
dispatch_semaphore 是信號量,但當信號總量設為 1 時也可以當作鎖來。在沒有等待情況出現時,它的性能比 pthread_mutex 還要高,但一旦有等待情況出現時,性能就會下降許多。相對于 OSSpinLock 來說,它的優勢在于等待時不會消耗 CPU 資源。對磁盤緩存來說,它比較合適。
為此我也特地補了下課,pthread_mutex
其實也是利用OSSpinLock
實現的,還有其他的一些鎖比如NSLock
、@synchronized
,這些使用也很方便,網上資料也很多。我簡單測試了下,OSSpinLock
相對性能最高,@synchronized
相對性能差些,具體的也可以自己實驗一下。
線程安全就是說多線程訪問同一代碼,不會產生不確定的結果。如果在執行代碼前加鎖,只有等這段代碼完成后才解鎖,這樣就不會出現因多線程而出現競爭資源等問題,從而實現線程安全。
雙向鏈表結構
我們先來看下鏈表的節點,可以看出主要是上一個節點指針
,下一個節點指針
,key值
,value值
,節點開銷大小
,緩存時間戳
等部分
@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;//緩存時間
}
我們再來看下鏈表的結構,代碼都添上了中文注釋:
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // 字典的Ref(理解成字典標示)
NSUInteger _totalCost; //鏈表總開銷
NSUInteger _totalCount; //鏈表個數
_YYLinkedMapNode *_head; // 鏈表首個節點指針
_YYLinkedMapNode *_tail; // 鏈表末尾節點指針
BOOL _releaseOnMainThread; //是否在主線程釋放內存
BOOL _releaseAsynchronously;//是否異步釋放內存
}
/// 插入一個節點,并且更新鏈表總開銷
/// Node and node.key should not be nil.
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
/// 將一個節點放到鏈表頂部
/// Node should already inside the dic.
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
/// 移除一個節點
/// Node should already inside the dic.
- (void)removeNode:(_YYLinkedMapNode *)node;
///移除尾部節點,淘汰數據
- (_YYLinkedMapNode *)removeTailNode;
/// 移除所有節點
- (void)removeAll;
@end
下面這張圖很好得解釋了整個鏈表結構,如果有數據結構基礎讀懂這個雙向鏈表應該很容易。
YYMemoryCache
由于代碼還是比較簡單的,所以我打算用在源碼上注釋的方式解釋,也就不畫流程圖了。
添加數據
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}
//這里開始加鎖
pthread_mutex_lock(&_lock);
//這句話代碼其實就是相當于 NSMutableDictionary objecyForKey,取出鏈表節點,這個NSMutableDictionary里面裝的是<_YYLinkedMapNode *>
_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);
}
取出數據
- (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);
//沒有返回Nil
return node ? node->_value : nil;
}
定時清理
這里就是區別普通NSDictionary緩存的地方之一,不斷在后臺更新緩存數據,清理過去數據,只要設置一個_autoTrimInterval
時間間隔就好。
- (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];
});
}
YYKVStorage
要解析YYDiskCache
首先得解析YYKVStorage
,我發現這里主要用了2種存儲方式,sqlLite
和文件存儲
。一開始并不明白為何這么做,后來參考網上資料:
該文件主要以兩種方式來實現磁盤存儲:SQLite、File,使用兩種方式混合進行存儲主要為了提高讀寫效率。寫入數據時,SQLite要比文件的方式更快;讀取數據的速度主要取決于文件的大小。據測試,在iPhone6中,當文件大小超過20kb時,File要比SQLite快的多。所以當大文件存儲時建議用File的方式,小文件更適合用SQLite。
所以,主要還是要顧及到存儲速度吧。
添加數據
- (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;
}
if (filename.length) {
// filename存在 SQLite File兩種方式并行
// 用文件進行存儲
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不存在采用SQLite進行存儲
if (_type != YYKVStorageTypeSQLite) {
// 這邊去到filename后,刪除filename對應的file文件
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
// SQLite 進行存儲
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
獲取數據
- (NSData *)getItemValueForKey:(NSString *)key {
if (key.length == 0) return nil;
NSData *value = nil;
switch (_type) {
case YYKVStorageTypeFile: { //File
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
// 根據filename獲取File
value = [self _fileReadWithName:filename];
if (!value) {
// 當value不存在,用對應的key刪除SQLite文件
[self _dbDeleteItemWithKey:key];
value = nil;
}
}
} break;
case YYKVStorageTypeSQLite: {
// SQLite 方式獲取
value = [self _dbGetValueWithKey:key];
} break;
case YYKVStorageTypeMixed: {
NSString *filename = [self _dbGetFilenameWithKey:key];
// filename 存在文件獲取,不存在SQLite方式獲取
if (filename) {
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和sqlLite方式進行存儲。
YYDiskCache
YYDiskCache
的核心內容就是 YYKVStorage
,它是YYKVStorage
的拓展。
存儲
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}
//獲取要擴展的數據信息(就是后面跟一段數據)
NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
NSData *value = nil;
if (_customArchiveBlock) {
//如果有定義customArchiveBlock這個block就回調
value = _customArchiveBlock(object);
} else {
@try {
//將數據對象解析成NSData
value = [NSKeyedArchiver archivedDataWithRootObject:object];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (!value) return;
NSString *filename = nil;
//這里的_kv就是上面提到的YYKVStorage類型
if (_kv.type != YYKVStorageTypeSQLite) {
if (value.length > _inlineThreshold) {
//如果數據長度達到一定條件就sqlite和文件存儲2種方式同時進行,這里的filename就是關鍵字md5加密
filename = [self _filenameForKey:key];
}
}
//設置鎖,這里的Lock是宏定義用的是dispatch_semaphore_wait
Lock();
//用YYKVStorage存儲
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
//解鎖
Unlock();
}
#pragma mark 用runtime添加擴展屬性
+ (NSData *)getExtendedDataFromObject:(id)object {
if (!object) return nil;
return (NSData *)objc_getAssociatedObject(object, &extended_data_key);
}
+ (void)setExtendedData:(NSData *)extendedData toObject:(id)object {
if (!object) return;
objc_setAssociatedObject(object, &extended_data_key, extendedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
獲取數據
- (id<NSCoding>)objectForKey:(NSString *)key {
if (!key) return nil;
Lock();
YYKVStorageItem *item = [_kv getItemForKey:key];
Unlock();
if (!item.value) return nil;
id object = nil;
if (_customUnarchiveBlock) {
object = _customUnarchiveBlock(item.value);
} else {
@try {
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (object && item.extendedData) {
[YYDiskCache setExtendedData:item.extendedData toObject:object];
}
return object;
}
總結
YYCache還是比較簡單的,解析起來并不難。也有很多值得學習的地方,比如線程安全
、sqlLite和文件并行存儲
、LRU算法的實現
。
參考文獻
http://www.cocoachina.com/ios/20160810/17335.html
http://blog.ibireme.com/2015/10/26/yycache/
我是翻滾的牛寶寶,歡迎大家評論交流~