一直想研究研究YYKit的源碼,最近剛好抽出了些時間開始看,希望這是一個系列的文章。說句題外話YYKit真的一個龐大的工具庫,涵蓋了我們日常絕大多數需要的工具,不僅僅是常用的YYModel、YYCache、YYImage、YYText還提供了NSString、NSObject、NSArray、NSNumber等的Category,提供了使用平率很高的工具方法,甚至提供了KVO的block形式的封裝,接入一個YYKit很多其他的第三方庫都不再需要接入了。
好了直入主題,下圖是YYCache的架構圖:

下面是YYCache的使用方法:
YYCache *cache = [YYCache cacheWithName:@"courseCache"];
[cache setObject:obj1 forKey:key1];
[cache containsObjectForKey:key1];
[cache objectForKey:key1];
...
默認情況下YYCache會在內存和文件/DB中分別緩存一份數據,取數據時先向內存取,若沒有名中則向文件/DB取,同時更新內存中的緩存,這一點和SDWebImage中對圖片的緩存機制是一致的。YYCache內部實現了LRU算法,然后通過總數量、總大小、存活時間這些指標配合LRU實現了淘汰緩存文件的機制。本文主要是介紹YYCache內部的LRU實現機制。
YYMemoryCache
YYMemoryCache內部用一個雙向鏈表實現了LRU算法。
鏈表節(jié)點_YYLinkedMapNode:
/**
A node in linked map.
*/
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // 指向前一個節(jié)點 retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // 指向后一個節(jié)點 retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
@implementation _YYLinkedMapNode
@end
雙向鏈表結構_YYLinkedMap:
/**
A linked map used by YYMemoryCache.
It's not thread-safe and does not validate the parameters.
*/
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // 實際持有節(jié)點的字典
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // 尾節(jié)點
_YYLinkedMapNode *_tail; // 頭結點
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}
下面是一些雙向鏈表的操作,就是一個典型的雙向鏈表的操作,相信讀者都能看明白,不做過多注釋 :)
頭部插入操作:
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
_totalCost += node->_cost;// cost增加
_totalCount++; //數量增加
if (_head) {
node->_next = _head;
_head->_prev = node;
_head = node;
} else {
_head = _tail = 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;
}
將一個節(jié)點移至頭結點:
- (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;
}
同時YYMemory內部有一個計時器,默認每隔5秒鐘檢查一次緩存的狀態(tài)(主要是總數量、總大小、存活時間),若超出則通過雙向鏈表刪除尾節(jié)點。
- (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];
});
}
至于這里為什么沒有用NSTimer,我也沒弄明白(攤手。還請知道的讀者賜教。
update: 2017.10.26
NSTimer必須放在runloop中才能生效,使用dispatch_after的方式可以避開這個限制。
YYDiskCache
YYDiskCache用文件和SQLite作為存儲介質。至于這兩者的規(guī)則,YYDiskCache.h有這樣一個參數:
@property (readonly) NSUInteger inlineThreshold;
簡而言之超過這個閾值會使用文件存儲,低于這個閾值會使用SQLite存儲,這個值默認是20KB。
但是注意,這里的文件存儲和SQLite存儲并不是指的數據完全用文件存儲或者SQLite存儲;具體的規(guī)則還是代碼比較好說明:
YYKVStorage.h
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0,
YYKVStorageTypeSQLite = 1,
YYKVStorageTypeMixed = 2,
};
YYDiskCache.m
- (instancetype)initWithPath:(NSString *)path inlineThreshold:(NSUInteger)threshold {
...
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}
...
}
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
...
NSString *filename = nil;
if (_kv.type != YYKVStorageTypeSQLite) {
if (value.length > _inlineThreshold) {
filename = [self _filenameForKey:key];//計算key的MD5值
}
}
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
YYKVStorage.m
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
...
if (filename.length) {
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
if (_type != YYKVStorageTypeSQLite) {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
oh no,我真的不想貼這么多源碼,也難為ibreme設計出這么一套規(guī)則,反正就一個目的,在SQLite是一定能找到緩存數據對應的key->filename的。至于真正的數據data(value)只有在type是YYKVStorageTypeFile才會在文件中存儲一份備份。至于為什么這么做?因為SQLite能用時間戳取排序取出數據來實現LRU淘汰算法。所以所有的緩存數據必須在SQLite中找到索引。
一些其他的技巧
宏
作者ibreme在源碼中使用了很多宏,比如NS_DESIGNATED_INITIALIZER UNAVAILABLE_ATTRIBUTE等等,UNAVAILABLE_ATTRIBUTE這個宏在封裝組件的時候尤其有用,當某各類的初始化強依賴于幾個必要的屬性的時候,可以禁用init方法和new方法 or whatever。下面是這個宏的使用姿勢:
YYCache.h
- (nullable instancetype)initWithName:(NSString *)name;
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
效果如下:

NSMapTable
日常開發(fā)中使用的NSDictionary、NSArray、NSSet對key和value都是強引用,但是在遇到需要對key或者value的指針是個弱引用的時候這個類就派上用場了:
[[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
YYDiskCache中由于想通過一個哈希表記錄path -> cache(YYDiskCache) 但是卻不想對cache形成強引用而使用了這個類。