YYCache源碼分析

YYCache是用于Objective-C中用于緩存的第三方框架。此文主要用來講解該框架的實(shí)現(xiàn)細(xì)節(jié),性能分析、設(shè)計思路等。


YYCache:同時實(shí)現(xiàn)內(nèi)存緩存和磁盤緩存且是線程安全的

YYMemoryCache:實(shí)現(xiàn)內(nèi)存緩存,所有的API都是線程安全的,與其他緩存方式比較不同的是內(nèi)部利用LRU淘汰算法(后面會介紹)來提高性能

YYDiskCache:實(shí)現(xiàn)磁盤緩存,所有的API都是線程安全的,內(nèi)部也采用了LRU淘汰算法,主要SQLite和文件存儲兩種方式

YYKVStorage:實(shí)現(xiàn)磁盤存儲,不推薦直接使用該類,該類不是線程安全的

LRU

LRU(Least recently used,最近最少使用)算法,根據(jù)訪問的歷史記錄來對數(shù)據(jù)進(jìn)行淘汰


簡單的來說3點(diǎn):

有新數(shù)據(jù)加入時添加到鏈表的頭部

每當(dāng)緩存命中(即緩存數(shù)據(jù)被訪問),則將數(shù)據(jù)移到鏈表頭部

當(dāng)鏈表滿的時候,將鏈表尾部的數(shù)據(jù)丟棄

在YYMemoryCache中使用來雙向鏈表和NSDictionary實(shí)現(xiàn)了LRU淘汰算法,后面會介紹

簡單的來說3點(diǎn):

有新數(shù)據(jù)加入時添加到鏈表的頭部

每當(dāng)緩存命中(即緩存數(shù)據(jù)被訪問),則將數(shù)據(jù)移到鏈表頭部

當(dāng)鏈表滿的時候,將鏈表尾部的數(shù)據(jù)丟棄

在YYMemoryCache中使用來雙向鏈表和NSDictionary實(shí)現(xiàn)了LRU淘汰算法,后面會介紹

關(guān)于鎖

YYCache 使用到兩種鎖

OSSpinLock :自旋鎖,上一篇博客也提及到pthread_mutex

dispatch_semaphore:信號量,當(dāng)信號量為1的時候充當(dāng)鎖來用

內(nèi)存緩存用的pthread_mutex:由于pthread_mutex相當(dāng)于do while忙等,等待時會消耗大量的CPU資源

磁盤緩存使用的dispatch_semaphore:優(yōu)勢在于等待時不會消耗CPU資源

簡單的科普就到這,現(xiàn)在來開始源碼的探索

_YYLinkedMap

@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

_YYLinkedMapNode:鏈表的節(jié)點(diǎn)

_prev、_next:分別表示指向上一個節(jié)點(diǎn)、下一個節(jié)點(diǎn)

_key:緩存的key

_value:緩存對象

_cost:內(nèi)存消耗

_time:緩存時間

@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(最近最少使用算法-清除較不常使用數(shù)據(jù)), do not change it directly

BOOL _releaseOnMainThread;

BOOL _releaseAsynchronously;

}

_YYLinkedMap:鏈表

_dic:用來保存節(jié)點(diǎn)

_totalCost:總緩存開銷

_head、_tail:頭節(jié)點(diǎn)、尾節(jié)點(diǎn)

_releaseOnMainThread:是否在主線程釋放_YYLinkedMapNode

_releaseAsynchronously:是否異步釋放_YYLinkedMapNode

雙向鏈表


插入節(jié)點(diǎn)到頭部

將除兩邊的節(jié)點(diǎn)移到頭部

移除除兩邊的節(jié)點(diǎn)

移除尾部節(jié)點(diǎn)

移除所有節(jié)點(diǎn)

看下移除所有節(jié)點(diǎn)的代碼:

- (void)removeAll {

_totalCost = 0;

_totalCount = 0;

_head = nil;

_tail = nil;

if (CFDictionaryGetCount(_dic) > 0) {

CFMutableDictionaryRef holder = _dic;

_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

if (_releaseAsynchronously) {

dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();

dispatch_async(queue, ^{

CFRelease(holder);? hold and release in specified queue

});

} else if (_releaseOnMainThread && !pthread_main_np()) {

dispatch_async(dispatch_get_main_queue(), ^{

CFRelease(holder);? hold and release in specified queue

});

} else {

CFRelease(holder);

}

}

}

這邊通過雙向鏈表來對數(shù)據(jù)進(jìn)行操作,和NSDictionary實(shí)現(xiàn)了LRU淘汰算法。時間復(fù)雜度0(1),5種操作基本上都是對頭尾節(jié)點(diǎn)和鏈表節(jié)點(diǎn)的上一個節(jié)點(diǎn)和下一個節(jié)點(diǎn)進(jìn)行操作,耐心看還是能簡單的,這邊就不分析了

YYMemoryCache

這邊介紹兩個主要的操作:添加緩存,查找緩存

添加緩存

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {

if (!key) return;

if (!object) {

緩存對象為nil,直接移除

[self removeObjectForKey:key];

return;

}

為了保證線程安全,數(shù)據(jù)操作前進(jìn)行加鎖

pthread_mutex_lock(&_lock);

查找緩存

_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));

當(dāng)前時間

NSTimeInterval now = CACurrentMediaTime();

if (node) {

緩存對象已存在,更新數(shù)據(jù),并移到棧頂

_lru->_totalCost -= node->_cost;

_lru->_totalCost += cost;

node->_cost = cost;

node->_time = now;

node->_value = object;

[_lru bringNodeToHead:node];

} else {

緩存對象不存在,添加數(shù)據(jù),并移到棧頂

node = [_YYLinkedMapNode new];

node->_cost = cost;

node->_time = now;

node->_key = key;

node->_value = object;

[_lru insertNodeAtHead:node];

}

判斷當(dāng)前的緩存進(jìn)行是否超出了設(shè)定值,若超出則進(jìn)行整理

if (_lru->_totalCost > _costLimit) {

dispatch_async(_queue, ^{

[self trimToCost:_costLimit];

});

}每次添加數(shù)據(jù)僅有一個,數(shù)量上超出時,直接移除尾部那個object即可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];

});

} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {

dispatch_async(dispatch_get_main_queue(), ^{

[node class];

});

}

}

操作結(jié)束,解鎖

pthread_mutex_unlock(&_lock);

}

異步線程釋放

里面很多都用到類似的方法,將一個對象在異步線程中釋放,來分析下:

- p

1. 首先通過node來對其進(jìn)行持有,以至于不會在方法調(diào)用結(jié)束的時候被銷毀

2. 我要要在其他線程中進(jìn)行銷毀,所以將銷毀操作放在block中,block就會對其進(jìn)行持有

3. 這邊在block中隨便調(diào)用了個方法,保證編譯器不會優(yōu)化掉這個操作

4. 當(dāng)block結(jié)束后,node沒有被持有的時候,就會在當(dāng)前線程被release掉了

添加緩存

這邊從memory中取數(shù)據(jù)時,根據(jù)LRU原則,將最新取出的object放到棧頭

- (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);

return node ? node->_value : nil;

}

YYKVStorage

該文件主要以兩種方式來實(shí)現(xiàn)磁盤存儲:SQLite、File,使用兩種方式混合進(jìn)行存儲主要為了提高讀寫效率。寫入數(shù)據(jù)時,SQLite要比文件的方式更快;讀取數(shù)據(jù)的速度主要取決于文件的大小。據(jù)測試,在iPhone6中,當(dāng)文件大小超過20kb時,F(xiàn)ile要比SQLite快的多。所以當(dāng)大文件存儲時建議用File的方式,小文件更適合用SQLite。

下邊分別對Save、Remove、Get分別進(jìn)行分析

Save

- (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兩種方式并行

用文件進(jìn)行存儲

if (![self _fileWriteWithName:filename data:value]) {

return NO;

}

用SQLite進(jìn)行存儲

if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {

當(dāng)使用SQLite方式存儲失敗時,刪除本地文件存儲

[self _fileDeleteWithName:filename];

return NO;

}

return YES;

} else {? ? ? ? ? ? ? filename不存在 SQLite

if (_type != YYKVStorageTypeSQLite) {

這邊去到filename后,刪除filename對應(yīng)的file文件

NSString *filename = [self _dbGetFilenameWithKey:key];

if (filename) {

[self _fileDeleteWithName:filename];

}

}

SQLite 進(jìn)行存儲

return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];

}

}

Remove

- (BOOL)removeItemForKey:(NSString *)key {

if (key.length == 0) return NO;

switch (_type) {

case YYKVStorageTypeSQLite: {

刪除SQLite文件

return [self _dbDeleteItemWithKey:key];

} break;

case YYKVStorageTypeFile:

case YYKVStorageTypeMixed: {

獲取filename

NSString *filename = [self _dbGetFilenameWithKey:key];

if (filename) {

刪除filename對的file

[self _fileDeleteWithName:filename];

}

刪除SQLite文件

return [self _dbDeleteItemWithKey:key];

} break;

default: return NO;

}

}

Get

- (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) {

根據(jù)filename獲取File

value = [self _fileReadWithName:filename];

if (!value) {

當(dāng)value不存在,用對應(yīng)的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方式主要使用的writeToFile進(jìn)行存儲,SQLte直接使用的sqlite3來對文件進(jìn)行操作,具體數(shù)據(jù)庫相關(guān)的操作這邊就不在進(jìn)行分析了,感興趣的自己可以閱讀下

YYDiskCache

YYDiskCache是對YYKVStorage進(jìn)行的一次封裝,是線程安全的,這邊使用的是dispatch_semaphore_signal來確保線程的安全。另外他結(jié)合LRU算法,根據(jù)文件的大小自動選擇存儲方式來達(dá)到更好的性能。

- (instancetype)initWithPath:(NSString *)path

inlineThreshold:(NSUInteger)threshold {

self = [super init];

if (!self) return nil;

獲取緩存的 YYDiskCache

YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);

if (globalCache) return globalCache;

確定存儲的方式

YYKVStorageType type;

if (threshold == 0) {

type = YYKVStorageTypeFile;

} else if (threshold == NSUIntegerMax) {

type = YYKVStorageTypeSQLite;

} else {

type = YYKVStorageTypeMixed;

}

初始化 YYKVStorage

YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];

if (!kv) return nil;

初始化數(shù)據(jù)

_kv = kv;

_path = path;

_lock = dispatch_semaphore_create(1);

_queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);

_inlineThreshold = threshold;

_countLimit = NSUIntegerMax;

_costLimit = NSUIntegerMax;

_ageLimit = DBL_MAX;

_freeDiskSpaceLimit = 0;

_autoTrimInterval = 60;

遞歸的去整理文件

[self _trimRecursively];

對當(dāng)前對象進(jìn)行緩存

_YYDiskCacheSetGlobal(self);

通知 APP即將被殺死時

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];

return self;

}

其他的一些操作基本上都是對YYKVStorage的一些封裝,這邊就不一一分析了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 作者的設(shè)計思路和細(xì)節(jié)參看這里:YYCache 設(shè)計思路 YY給我印象最深的是他做事的方式,文章里寫到他調(diào)研了多個相...
    lanjing閱讀 280評論 0 0
  • YYCache是用于Objective-C中用于緩存的第三方框架。此文主要用來講解該框架的實(shí)現(xiàn)細(xì)節(jié),性能分 ...
    愛敲代碼的果果閱讀 522評論 2 3
  • YYCache源碼分析(三) 本文分析YYDiskCache->YYKVStorage實(shí)現(xiàn)過程:YYDiskCac...
    kakukeme閱讀 790評論 0 49
  • 技術(shù)無極限,從菜鳥開始,從源碼開始。 由于公司目前項(xiàng)目還是用OC寫的項(xiàng)目,沒有升級swift 所以暫時SDWebI...
    充滿活力的早晨閱讀 12,830評論 0 2
  • 2016-06-14 08:11編輯:cocopeng分類:iOS開發(fā)來源:漢斯哈哈哈的簡書 34436 iOSY...
    橙娃閱讀 803評論 0 1

友情鏈接更多精彩內(nèi)容