YYCache源碼分析

YYMemoryCache


YYMemoryCache用于對內(nèi)存緩存進(jìn)行管理,與SDWebImage對于內(nèi)存緩存管理策略的區(qū)別是,SDWebImage對于內(nèi)存緩存的管理是基于系統(tǒng)的NSCache類,而YYMemoryCache是基于作者自定義的雙向鏈表,并基于鏈表自定義了一套淘汰算法來對內(nèi)存使用進(jìn)行性能優(yōu)化。

_YYLinkedMap和_YYLinkedMapNode

既然有自定義鏈表,必然也有自定義的鏈表節(jié)點,關(guān)于鏈表節(jié)點聲明如下:
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end

@implementation _YYLinkedMapNode
@end

既然_YYLinkedMap是雙向鏈表,那么結(jié)點_YYLinkedMapNode的數(shù)據(jù)結(jié)構(gòu)中必然會有兩個指針,_prev指向鏈接當(dāng)前結(jié)點的上一個結(jié)點,_next指向當(dāng)前結(jié)點鏈接的下一個結(jié)點。

_key可以理解為它是當(dāng)前緩存數(shù)據(jù)的標(biāo)識符,比方對于UIImage對象來說,它的key就是URL,那么_value就是這個UIImage對象。

_cost表示當(dāng)前緩存數(shù)據(jù)的內(nèi)存占用情況,打個比方,在YYImageCahce中對于UIImage對象的內(nèi)存計算是通過獲取圖片的高度和行數(shù)再將這兩個數(shù)據(jù)進(jìn)行相乘的結(jié)果,代碼如下:

- (NSUInteger)imageCost:(UIImage *)image {
    CGImageRef cgImage = image.CGImage;
    if (!cgImage) return 1;
    CGFloat height = CGImageGetHeight(cgImage);
    size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
    NSUInteger cost = bytesPerRow * height;
    if (cost == 0) cost = 1;
    return cost;
}

而在SDWebImageSDImageCache中,對于UIImage的內(nèi)存計算是UIImagewidthheight以及scale相乘的結(jié)果:

FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
    return image.size.height * image.size.width * image.scale * image.scale;
}

最后結(jié)點還有一個成員變量_time,表示當(dāng)前結(jié)點的生命周期。

關(guān)于自定義鏈表:
@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _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;
    BOOL _releaseAsynchronously;
}

成員變量_dic用于存放_YYLinkedMapNode結(jié)點數(shù)據(jù),key就是結(jié)點的key值,value就是結(jié)點本身。另外_dic的類型是Core Foundation層的CFMutableDictionaryRef

關(guān)于_totalCost_totalCount這兩個變量,意思和NSCache中的totalCostLimitcountLimit差不多,_totalCost表示圖片的內(nèi)存總占用,_totalCount表示總的個數(shù),這兩個變量用于后續(xù)實現(xiàn)淘汰算法。

關(guān)于_head_tail變量,這兩個變量的類型都是_YYLinkedMapNode,后面的注釋已經(jīng)解釋了這兩個變量的作用,
_head是最近使用較多的結(jié)點(MRU),_tail最近使用最少的結(jié)點(LRU)。

關(guān)于_releaseOnMainThread_releaseAsynchronously變量,如果_releaseOnMainThread = YES,就會在主線程釋放_dic變量,如果_releaseAsynchronously = YES,就會獲取一個專門用來做release操作的異步隊列來釋放_dic

_YYLinkedMap也提供了一些操作結(jié)點的方法:

1.向表頭插入一個結(jié)點
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

2.將鏈表內(nèi)某個結(jié)點移至表頭
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

3.刪除某個結(jié)點
- (void)removeNode:(_YYLinkedMapNode *)node;

4.刪除使用最少的結(jié)點
- (_YYLinkedMapNode *)removeTailNode;

5.刪除所有結(jié)點
- (void)removeAll;

YYMemoryCache

@implementation YYMemoryCache {
    pthread_mutex_t _lock;
    _YYLinkedMap *_lru;
    dispatch_queue_t _queue;
}

YYMemoryCache初始化做了這些事:
1、初始化互斥鎖_lock
2、初始化_lru
3、初始化串行隊列_queue
4、對_countLimit、_costLimit、_ageLimit、_autoTrimInterval設(shè)置默認(rèn)值。
5、對_shouldRemoveAllObjectsOnMemoryWarning_shouldRemoveAllObjectsWhenEnteringBackground設(shè)置默認(rèn)值為YES,接收到內(nèi)存警告或程序進(jìn)入后臺都會清空內(nèi)存。

YYMemoryCache提供了一些訪問方法:

#pragma mark - Access Methods
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;

#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;

Access Methods下的幾個方法都是操作鏈表_lru及其結(jié)點。

trim下的3個方法就是淘汰算法的實現(xiàn),也是對雙向鏈表的操作代碼,淘汰的緯度有3個,包括內(nèi)存管理容器所存儲結(jié)點的數(shù)量、結(jié)點的開銷、結(jié)點的使用頻率等。

這里有個tip,同時也是作者分享的,就是讓block捕獲一個局部變量,然后扔到后臺隊列去隨便發(fā)送個消息以避免編譯器警告,這樣就可以讓對象在后臺線程銷毀:

NSMutableArray *holder = [NSMutableArray new];

if (holder.count) {
    dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
    dispatch_async(queue, ^{
         [holder count]; // release in queue
    });
}

作者還提供了一個看起來簡單點的實現(xiàn)方式:

NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
    [tmp class];
});

另外,在YYMemoryCache初始化方法當(dāng)中,會調(diào)用_trimRecursively方法,該方法每隔5秒中就會調(diào)用自己一次,另外,在這個方法中還會調(diào)用調(diào)用Trim下的3個方法對LRU進(jìn)行清理,以節(jié)省內(nèi)存。

以上就是YYMemoryCache的實現(xiàn)。

YYKVStorage


在了解YYDiskCache的實現(xiàn)原理前需要先了解一下YYKVStorage

作者是這樣介紹YYKVStorage的:
YYKVStorage是一個基于sqlite和文件系統(tǒng)的鍵值存儲。但作者不建議我們直接使用此類(ps:這個類被封裝到了YYDiskCache里,可以通過YYDiskCache間接使用此類),這個類只有一個初始化方法,即initWithPath:type:,初始化后,講根據(jù)path創(chuàng)建一個目錄來保存鍵值數(shù)據(jù),初始化后,如果沒有得到當(dāng)前類的實例對象,就表示你不應(yīng)該對改目錄進(jìn)行讀寫操作。最后,作者還寫了個警告,告訴我們這個類的實例對象并不是線程安全的,你需要確保同一時間只有一條線程訪問實例對象,如果你確實需要在多線程中處理大量數(shù)據(jù),可以把數(shù)據(jù)拆分到多個實例對象當(dāng)中去。

YYKVStorage提供了一些public方法用于操作sqlite和文件系統(tǒng),這些方法覆蓋了增刪改查這4個操作。

基于sqlite的鍵值存儲

YYKVStorage實現(xiàn)了對sqlite的封裝,包括數(shù)據(jù)庫初始化、打開數(shù)據(jù)庫、關(guān)閉數(shù)據(jù)庫、執(zhí)行sql等操作。這個類中絕大多數(shù)代碼也是對這些操作的實現(xiàn)代碼。

為了加強(qiáng)數(shù)據(jù)庫檢索時的性能,在建表的同時又為表建立了索引。

表名叫manifest,表內(nèi)有幾個字段:

key:主鍵,增刪改查操作都圍繞這個主鍵來完成
filename:文件名稱
size:文件大小
inline_data:存儲的二進(jìn)制數(shù)據(jù)
modification_time:文件修改時間
last_access_time:文件最后訪問時間
extended_data:文件擴(kuò)建時間
1. 保存

方法:saveItemWithKey:value:filename:extendedData:

數(shù)據(jù)保存的目標(biāo)地點分為兩種,一種是直接放在沙盒指定目錄下,另外一種是存儲在數(shù)據(jù)庫(雖然db也是放在沙盒里的),具體采用哪種目標(biāo)地點會根據(jù)filename做判斷,如果filename存在即表明數(shù)據(jù)是直接存儲在沙盒某個目錄下的文件里,如果filename不存在就會走和數(shù)據(jù)庫相關(guān)的流程。

如果是寫入文件:
  • 會根據(jù)filename生成完整的存儲路徑,再把value寫入到目標(biāo)文件中。
    • 如果寫入成功不再執(zhí)行后續(xù)代碼。
    • 如果寫入失敗,則嘗試把數(shù)據(jù)寫入到數(shù)據(jù)庫中。
      • 如果寫入成功不再執(zhí)行后續(xù)代碼;
      • 如果數(shù)據(jù)庫也寫入失敗了,就會刪除前面生成的存儲路徑下的文件,避免產(chǎn)生垃圾文件。
如果是寫入數(shù)據(jù)庫:
  • 首先會判斷當(dāng)前的storage對象是不是用來做數(shù)據(jù)庫緩存操作的實例對象。
    • 如果是,根據(jù)方法傳入的參數(shù)重新向數(shù)據(jù)庫中插入數(shù)據(jù)。
    • 如果不是,根據(jù)key到數(shù)據(jù)庫里查詢對應(yīng)的filename,根據(jù)filename刪除相應(yīng)路徑下的文件,最后根據(jù)方法傳入的參數(shù)重新向數(shù)據(jù)庫中插入數(shù)據(jù)。
2. 查詢
方式一:直接獲取二進(jìn)制數(shù)據(jù):
(1)從文件中查詢:

1.根據(jù)key從數(shù)據(jù)庫查找到對應(yīng)的filename
2.根據(jù)步驟1中查詢到的filename生成完成的文件存儲路徑并讀取文件數(shù)據(jù),更新數(shù)據(jù)中該條數(shù)據(jù)的訪問時間。
3.如果步驟2中沒讀取到數(shù)據(jù),則把這條數(shù)據(jù)從數(shù)據(jù)庫中刪除。

(2)從數(shù)據(jù)庫中查詢:

1.直接根據(jù)key到數(shù)據(jù)庫中查詢inline_data,這個inline_data對應(yīng)著YYKVStorageItem中的value屬性。
2.更新數(shù)據(jù)中該條數(shù)據(jù)的訪問時間

(3)混合查詢(文件&數(shù)據(jù)庫):

1.根據(jù)key找到filename
2.filename存在讀取文件數(shù)據(jù),如果不存在數(shù)據(jù)則刪除文件。
3.filename不存在則去數(shù)據(jù)庫中通過key獲取inline_data
4.更新數(shù)據(jù)中該條數(shù)據(jù)的訪問時間

以上3個查詢操作獲取的均是單純的二進(jìn)制數(shù)據(jù),這些二進(jìn)制數(shù)據(jù)可能是由NSStringUIImage等對象轉(zhuǎn)換而來,調(diào)用者可根據(jù)需要自己轉(zhuǎn)換回原來的數(shù)據(jù)類型。

方式二:把獲取到的數(shù)據(jù)封裝成YYKVStorageItem

1.根據(jù)key到數(shù)據(jù)庫中查找數(shù)據(jù),根據(jù)數(shù)據(jù)生成YYKVStorageItem實例對象。
2.如果1中獲取的對象存在則同時更新當(dāng)前數(shù)據(jù)的last_access_time(最后訪問時間)。
3.通過拿到的filename生成文件路徑,讀取該文件,獲取文件內(nèi)存儲的二進(jìn)制數(shù)據(jù)。
4.如果二進(jìn)制數(shù)據(jù)不存在,就把該數(shù)據(jù)從數(shù)據(jù)庫中刪除,最后返回這個YYKVStorageItem實例對象。

YYDiskCache


知道YYKVStorage做什么之后,再來看YYDiskCache就簡單了。

作者這樣介紹YYDiskCache
YYDiskCache是一個線程安全的緩存,用于存儲SQLite支持的鍵值對和文件系統(tǒng)(類似于NSURLCache的磁盤緩存)。

YYDiskCache具有以下功能:
1.它使用LRU(最近最少使用)來刪除對象。
2.它可以通過成本,計數(shù)和年齡來控制。
3.它可以配置為在沒有可用磁盤空間時自動驅(qū)逐對象。
4.它可以自動決定每個對象的存儲類型(sqlite / file)。

總結(jié)一下就是:
YYDiskCache封裝了YYKVStorage,在YYDiskCache中對于disk的緩存操作實際上都是通過YYKVStorage完成的,除此之外,YYDiskCache又自定義了淘汰規(guī)則,刪除那些最近時間段內(nèi)不常用的對象。

YYCache


YYCache封裝了YYMemoryCacheYYDiskCache

YYCache初始化需要一個NSString類型的namepath,它會根據(jù)這兩個值生成一個路徑,根據(jù)這個路徑初始化出YYDiskCache

所以,接下來的事情就好辦了。

如果是存儲操作,YYCache首先會通過YYMemoryCache放進(jìn)內(nèi)存緩存,然后通過YYDiskCache放進(jìn)磁盤緩存。

如果是查詢操作,YYCache首先會通過YYMemoryCache先到內(nèi)存緩存中取,如果內(nèi)存緩存中沒有,再通過YYDiskCache到磁盤緩存中取。

如果是刪除操作,YYCache首先會通過YYMemoryCache刪除內(nèi)存緩存的數(shù)據(jù),然后通過YYDiskCache刪除磁盤緩存的數(shù)據(jù)。

總結(jié)


YYCache自定義了內(nèi)存緩存和磁盤緩存類,并實現(xiàn)了各自的淘汰算法,在時間和空間上對數(shù)據(jù)緩存操作都進(jìn)行了優(yōu)化。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,367評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,001評論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,213評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,535評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,317評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,868評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,963評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,090評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,599評論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,549評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,712評論 1 367
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,233評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,961評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,353評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,607評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,321評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,686評論 2 370

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

  • 從 YYCache 源碼 Get 到如何設(shè)計一個優(yōu)秀的緩存 來源:Lision 前言 iOS 開發(fā)中總會用到各種緩...
    今天lgw閱讀 6,048評論 1 22
  • YYCache是用于Objective-C中用于緩存的第三方框架。此文主要用來講解該框架的實現(xiàn)細(xì)節(jié),性能分析、設(shè)計...
    JonesCxy閱讀 575評論 0 2
  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 31,985評論 2 89
  • YYCache源碼分析(二) 原文鏈接:http://www.lxweimin.com/p/492c3c3a0485...
    kakukeme閱讀 559評論 0 51
  • 安裝 官方網(wǎng)站Charles 是一款收費(fèi)軟件,可以免費(fèi)體驗30天。網(wǎng)上有破解版。 使用 infoq 上有一篇很棒的...
    plusend閱讀 1,064評論 0 0