YYKit源碼分析---YYCache

YYCache是用于Objective-C中用于緩存的第三方框架。此文主要用來(lái)講解該框架的實(shí)現(xiàn)細(xì)節(jié),性能分析、設(shè)計(jì)思路ibireme已經(jīng)講得很清楚了,我這邊就不在分析了。首推個(gè)人blog:iipanda.com,同步更新。

文件結(jié)構(gòu)

  1. YYCache:同時(shí)實(shí)現(xiàn)內(nèi)存緩存和磁盤緩存且是線程安全的
  2. YYMemoryCache:實(shí)現(xiàn)內(nèi)存緩存,所有的API都是線程安全的,與其他緩存方式比較不同的是內(nèi)部利用LRU淘汰算法(后面會(huì)介紹)來(lái)提高性能
  3. YYDiskCache:實(shí)現(xiàn)磁盤緩存,所有的API都是線程安全的,內(nèi)部也采用了LRU淘汰算法,主要SQLite和文件存儲(chǔ)兩種方式
  4. YYKVStorage:實(shí)現(xiàn)磁盤存儲(chǔ),不推薦直接使用該類,該類不是線程安全的

LRU

LRU(Least recently used,最近最少使用)算法,根據(jù)訪問(wèn)的歷史記錄來(lái)對(duì)數(shù)據(jù)進(jìn)行淘汰
<p>



<p>
簡(jiǎn)單的來(lái)說(shuō)3點(diǎn):

  1. 有新數(shù)據(jù)加入時(shí)添加到鏈表的頭部
  2. 每當(dāng)緩存命中(即緩存數(shù)據(jù)被訪問(wèn)),則將數(shù)據(jù)移到鏈表頭部
  3. 當(dāng)鏈表滿的時(shí)候,將鏈表尾部的數(shù)據(jù)丟棄

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

關(guān)于鎖

YYCache 使用到兩種鎖

  1. OSSpinLock :自旋鎖,上一篇博客也提及到pthread_mutex
  2. dispatch_semaphore:信號(hào)量,當(dāng)信號(hào)量為1的時(shí)候充當(dāng)鎖來(lái)用

內(nèi)存緩存用的pthread_mutex:由于pthread_mutex相當(dāng)于do while忙等,等待時(shí)會(huì)消耗大量的CPU資源
磁盤緩存使用的dispatch_semaphore:優(yōu)勢(shì)在于等待時(shí)不會(huì)消耗CPU資源

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

_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:分別表示指向上一個(gè)節(jié)點(diǎn)、下一個(gè)節(jié)點(diǎn)

_key:緩存的key

_value:緩存對(duì)象

_cost:內(nèi)存消耗

_time:緩存時(shí)間

@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:用來(lái)保存節(jié)點(diǎn)

_totalCost:總緩存開銷

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

_releaseOnMainThread:是否在主線程釋放_(tái)YYLinkedMapNode

_releaseAsynchronously:是否異步釋放_(tái)YYLinkedMapNode

雙向鏈表

  1. 插入節(jié)點(diǎn)到頭部
  2. 將除兩邊的節(jié)點(diǎn)移到頭部
  3. 移除除兩邊的節(jié)點(diǎn)
  4. 移除尾部節(jié)點(diǎn)
  5. 移除所有節(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);
        }
    }
}


這邊通過(guò)雙向鏈表來(lái)對(duì)數(shù)據(jù)進(jìn)行操作,和NSDictionary實(shí)現(xiàn)了LRU淘汰算法。時(shí)間復(fù)雜度0(1),5種操作基本上都是對(duì)頭尾節(jié)點(diǎn)和鏈表節(jié)點(diǎn)的上一個(gè)節(jié)點(diǎn)和下一個(gè)節(jié)點(diǎn)進(jìn)行操作。

YYMemoryCache

這邊介紹兩個(gè)主要的操作:添加緩存,查找緩存<p>

  • 添加緩存
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;
    if (!object) {
        // 緩存對(duì)象為nil,直接移除
        [self removeObjectForKey:key];
        return;
    }
    // 為了保證線程安全,數(shù)據(jù)操作前進(jìn)行加鎖
    pthread_mutex_lock(&_lock);
    // 查找緩存
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    // 當(dāng)前時(shí)間
    NSTimeInterval now = CACurrentMediaTime();
    if (node) {
        // 緩存對(duì)象已存在,更新數(shù)據(jù),并移到棧頂
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        [_lru bringNodeToHead:node];
    } else {
        // 緩存對(duì)象不存在,添加數(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ù)僅有一個(gè),數(shù)量上超出時(shí),直接移除尾部那個(gè)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]; //hold and release in queue
            });
        } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [node class]; //hold and release in queue
            });
        }
    }
    // 操作結(jié)束,解鎖
    pthread_mutex_unlock(&_lock);
}

  • 異步線程釋放

    里面很多都用到類似的方法,將一個(gè)對(duì)象在異步線程中釋放,來(lái)分析下:
    - p
    1. 首先通過(guò)node來(lái)對(duì)其進(jìn)行持有,以至于不會(huì)在方法調(diào)用結(jié)束的時(shí)候被銷毀
    2. 我要要在其他線程中進(jìn)行銷毀,所以將銷毀操作放在block中,block就會(huì)對(duì)其進(jìn)行持有
    3. 這邊在block中隨便調(diào)用了個(gè)方法,保證編譯器不會(huì)優(yōu)化掉這個(gè)操作
    4. 當(dāng)block結(jié)束后,node沒(méi)有被持有的時(shí)候,就會(huì)在當(dāng)前線程被release掉了
  • 添加緩存
// 這邊從memory中取數(shù)據(jù)時(shí),根據(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

該文件主要以兩種方式來(lái)實(shí)現(xiàn)磁盤存儲(chǔ):SQLite、File,使用兩種方式混合進(jìn)行存儲(chǔ)主要為了提高讀寫效率。寫入數(shù)據(jù)時(shí),SQLite要比文件的方式更快;讀取數(shù)據(jù)的速度主要取決于文件的大小。據(jù)測(cè)試,在iPhone6中,當(dāng)文件大小超過(guò)20kb時(shí),F(xiàn)ile要比SQLite快的多。所以當(dāng)大文件存儲(chǔ)時(shí)建議用File的方式,小文件更適合用SQLite。<p>
下邊分別對(duì)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)行存儲(chǔ)
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        // 用SQLite進(jìn)行存儲(chǔ)
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            // 當(dāng)使用SQLite方式存儲(chǔ)失敗時(shí),刪除本地文件存儲(chǔ)
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {               // filename不存在 SQLite
        if (_type != YYKVStorageTypeSQLite) {
            // 這邊去到filename后,刪除filename對(duì)應(yīng)的file文件
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        // SQLite 進(jìn)行存儲(chǔ)
        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對(duì)的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不存在,用對(duì)應(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) {
        // 更新文件操作時(shí)間
        [self _dbUpdateAccessTimeWithKey:key];
    }
    return value;
}

File方式主要使用的writeToFile進(jìn)行存儲(chǔ),SQLte直接使用的sqlite3來(lái)對(duì)文件進(jìn)行操作,具體數(shù)據(jù)庫(kù)相關(guān)的操作這邊就不在進(jìn)行分析了,感興趣的自己可以閱讀下

YYDiskCache

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

- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    self = [super init];
    if (!self) return nil;
    
    // 獲取緩存的 YYDiskCache
    YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if (globalCache) return globalCache;
    
    // 確定存儲(chǔ)的方式
    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];
    // 對(duì)當(dāng)前對(duì)象進(jìn)行緩存
    _YYDiskCacheSetGlobal(self);
    
    // 通知 APP即將被殺死時(shí)
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
    return self;
}

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

參考文獻(xiàn)

  1. http://blog.ibireme.com/2015/10/26/yycache/
  2. http://blog.csdn.net/yunhua_lee/article/details/7599671
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,682評(píng)論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,435評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,665評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,838評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,379評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,627評(píng)論 2 380

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