YYCache初探

一直想研究研究YYKit的源碼竿秆,最近剛好抽出了些時間開始看启摄,希望這是一個系列的文章。說句題外話YYKit真的一個龐大的工具庫幽钢,涵蓋了我們?nèi)粘=^大多數(shù)需要的工具歉备,不僅僅是常用的YYModel、YYCache匪燕、YYImage蕾羊、YYText還提供了NSString、NSObject帽驯、NSArray龟再、NSNumber等的Category,提供了使用平率很高的工具方法尼变,甚至提供了KVO的block形式的封裝利凑,接入一個YYKit很多其他的第三方庫都不再需要接入了。

好了直入主題嫌术,下圖是YYCache的架構(gòu)圖:

YYCache架構(gòu)圖.jpg

下面是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;

效果如下:

禁用宏.jpg

NSMapTable

日常開發(fā)中使用的NSDictionaryNSArray耕腾、NSSet對key和value都是強(qiáng)引用见剩,但是在遇到需要對key或者value的指針是個弱引用的時候這個類就派上用場了:

[[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

YYDiskCache中由于想通過一個哈希表記錄path -> cache(YYDiskCache) 但是卻不想對cache形成強(qiáng)引用而使用了這個類。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末扫俺,一起剝皮案震驚了整個濱河市苍苞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌狼纬,老刑警劉巖羹呵,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異疗琉,居然都是意外死亡冈欢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門盈简,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凑耻,“玉大人太示,你說我怎么就攤上這事∠愫疲” “怎么了类缤?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長弃衍。 經(jīng)常有香客問我呀非,道長,這世上最難降的妖魔是什么镜盯? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任岸裙,我火速辦了婚禮,結(jié)果婚禮上速缆,老公的妹妹穿的比我還像新娘降允。我一直安慰自己,他們只是感情好艺糜,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布剧董。 她就那樣靜靜地躺著,像睡著了一般破停。 火紅的嫁衣襯著肌膚如雪翅楼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天真慢,我揣著相機(jī)與錄音毅臊,去河邊找鬼。 笑死黑界,一個胖子當(dāng)著我的面吹牛管嬉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播朗鸠,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蚯撩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了烛占?” 一聲冷哼從身側(cè)響起胎挎,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎忆家,沒想到半個月后犹菇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弦赖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了浦辨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹬竖。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡沼沈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出币厕,到底是詐尸還是另有隱情列另,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布旦装,位于F島的核電站页衙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏阴绢。R本人自食惡果不足惜店乐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望呻袭。 院中可真熱鬧眨八,春花似錦、人聲如沸左电。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽篓足。三九已至段誊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間栈拖,已是汗流浹背连舍。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辱魁,地道東北人烟瞧。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像染簇,于是被迫代替她去往敵國和親参滴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容

  • YYCache簡介 YYCache由YYMemoryCache(高速內(nèi)存緩存)和YYDiskCache(低速磁盤緩...
    簡書lu閱讀 1,454評論 0 5
  • YYCache是用于Objective-C中用于緩存的第三方框架锻弓。此文主要用來講解該框架的實(shí)現(xiàn)細(xì)節(jié)砾赔,性能分析、設(shè)計(jì)...
    JonesCxy閱讀 564評論 0 2
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫青灼、插件暴心、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,119評論 4 61
  • 作者設(shè)計(jì)思路 1.YYMemoryCache YYMemoryCache負(fù)責(zé)管理內(nèi)存緩存。這個類是線程安全的杂拨。 L...
    WeiHing閱讀 652評論 0 7
  • 0专普、前提"安裝CocoaPods 因?yàn)樽罱鼉商煳腋鼡Q了ssd固態(tài)硬盤和重裝了 macOS Sierra 10.12...
    朝雨晚風(fēng)閱讀 22,239評論 0 2