一直想研究研究YYKit的源碼竿秆,最近剛好抽出了些時間開始看启摄,希望這是一個系列的文章。說句題外話YYKit真的一個龐大的工具庫幽钢,涵蓋了我們?nèi)粘=^大多數(shù)需要的工具歉备,不僅僅是常用的YYModel、YYCache匪燕、YYImage蕾羊、YYText還提供了NSString、NSObject帽驯、NSArray龟再、NSNumber等的Category,提供了使用平率很高的工具方法尼变,甚至提供了KVO的block形式的封裝利凑,接入一個YYKit很多其他的第三方庫都不再需要接入了。
好了直入主題嫌术,下圖是YYCache的架構(gòu)圖:
下面是YYCache的使用方法:
YYCache *cache = [YYCache cacheWithName:@"courseCache"];
[cache setObject:obj1 forKey:key1];
[cache containsObjectForKey:key1];
[cache objectForKey:key1];
...
默認(rèn)情況下YYCache會在內(nèi)存和文件/DB中分別緩存一份數(shù)據(jù)哀澈,取數(shù)據(jù)時先向內(nèi)存取,若沒有名中則向文件/DB取度气,同時更新內(nèi)存中的緩存割按,這一點(diǎn)和SDWebImage中對圖片的緩存機(jī)制是一致的。YYCache內(nèi)部實(shí)現(xiàn)了LRU算法磷籍,然后通過總數(shù)量适荣、總大小、存活時間這些指標(biāo)配合LRU實(shí)現(xiàn)了淘汰緩存文件的機(jī)制院领。本文主要是介紹YYCache內(nèi)部的LRU實(shí)現(xiàn)機(jī)制束凑。
YYMemoryCache
YYMemoryCache內(nèi)部用一個雙向鏈表實(shí)現(xiàn)了LRU算法。
鏈表節(jié)點(diǎn)_YYLinkedMapNode
:
/**
A node in linked map.
*/
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // 指向前一個節(jié)點(diǎn) retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // 指向后一個節(jié)點(diǎn) retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
@implementation _YYLinkedMapNode
@end
雙向鏈表結(jié)構(gòu)_YYLinkedMap
:
/**
A linked map used by YYMemoryCache.
It's not thread-safe and does not validate the parameters.
*/
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // 實(shí)際持有節(jié)點(diǎn)的字典
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // 尾節(jié)點(diǎn)
_YYLinkedMapNode *_tail; // 頭結(jié)點(diǎn)
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}
下面是一些雙向鏈表的操作栅盲,就是一個典型的雙向鏈表的操作,相信讀者都能看明白废恋,不做過多注釋 :)
頭部插入操作:
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
_totalCost += node->_cost;// cost增加
_totalCount++; //數(shù)量增加
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é)點(diǎn)移至頭結(jié)點(diǎn):
- (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內(nèi)部有一個計(jì)時器谈秫,默認(rèn)每隔5秒鐘檢查一次緩存的狀態(tài)(主要是總數(shù)量、總大小鱼鼓、存活時間)拟烫,若超出則通過雙向鏈表刪除尾節(jié)點(diǎn)。
- (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作為存儲介質(zhì)于樟。至于這兩者的規(guī)則,YYDiskCache.h
有這樣一個參數(shù):
@property (readonly) NSUInteger inlineThreshold;
簡而言之超過這個閾值會使用文件存儲拇囊,低于這個閾值會使用SQLite存儲迂曲,這個值默認(rèn)是20KB。
但是注意寥袭,這里的文件存儲和SQLite存儲并不是指的數(shù)據(jù)完全用文件存儲或者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];//計(jì)算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設(shè)計(jì)出這么一套規(guī)則杰扫,反正就一個目的,在SQLite是一定能找到緩存數(shù)據(jù)對應(yīng)的key->filename的膘掰。至于真正的數(shù)據(jù)data(value)只有在type是YYKVStorageTypeFile才會在文件中存儲一份備份章姓。至于為什么這么做炭序?因?yàn)镾QLite能用時間戳取排序取出數(shù)據(jù)來實(shí)現(xiàn)LRU淘汰算法啤覆。所以所有的緩存數(shù)據(jù)必須在SQLite中找到索引。
一些其他的技巧
宏
作者ibreme在源碼中使用了很多宏惭聂,比如NS_DESIGNATED_INITIALIZER
UNAVAILABLE_ATTRIBUTE
等等窗声,UNAVAILABLE_ATTRIBUTE
這個宏在封裝組件的時候尤其有用,當(dāng)某各類的初始化強(qiáng)依賴于幾個必要的屬性的時候辜纲,可以禁用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都是強(qiáng)引用见剩,但是在遇到需要對key或者value的指針是個弱引用的時候這個類就派上用場了:
[[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
YYDiskCache中由于想通過一個哈希表記錄path -> cache(YYDiskCache) 但是卻不想對cache形成強(qiáng)引用而使用了這個類。