iOS本地緩存方案之YYCache源碼解析

iOS持久化方案有哪些?

簡單列舉一下,iOS的本地緩存方案有挺多,各有各的適用場景:html

  • NSUserDefault :

    系統提供的最簡便的key-value本地存儲方案,適合比較輕量的數據存儲,好比一些業務flag。主要緣由仍是其底層是用plist文件存儲的,在數據量逐步變大後,可能會發生性能問題。面試

  • 存文件,歸檔:

    不管是本身轉換業務數據爲二進制再writeFile,仍是直接利用系統的NSKeyedArchiver接口歸檔成文件,都屬於文件存儲的方案。優點是開發簡單,業務能夠自行控制單文件的存儲內容以免可能發生的性能問題。算法

  • sqlite、FMDB:

    底層利用到數據的存儲方案,比較適用數據量大,有查詢,排序等需求的存儲場景,缺點就是開發略複雜一些。sql

  • CoreData、其餘ORM方案:

    CoreData感受好像應用並非很普遍?數據庫

  • Key-Value接口的緩存方案:

    這裏特指提供Key-Value形式接口的緩存庫,底層緩存可能使用文件或者sqlite都有。本文討論的YYCache底層是混合使用文件+sqlite的存儲方式。基於接口簡便,性能優於NSUserDefault的特性,應該適用於大多數的業務場景,可是沒法適用上面數據庫相似的使用場景。緩存

聊聊YYCache的優秀設計

這裏其實yy大神本人在博文《YYCache 設計思路》中對其設計思路有比較詳盡的介紹,建議你們能夠先去讀一讀,本文就其相對於其餘緩存庫的一些優點點聊一聊。安全

高性能的線程安全方案

首先高性能是YYCache比較核心的一個設計目標,挺多代碼邏輯都是圍繞性能這個點來作的。微信

做爲對比,yy提出了TMMemoryCache方案的性能缺陷。TMMemoryCache的線程安全採用的是比較常見的經過dispatch_barrier來保障並行讀,串行寫的方案。該方案我在上一篇《AFNetworking源碼解析與面試考點思考》中有介紹。那麼TMMemoryCache存在性能問題的緣由會是由於其dispatch_barrier的線程安全方案嗎?多線程

答案應該在其同步接口的設計上:併發

- (id)objectForKey:(NSString *)key
{
    if (!key)
        return nil;

    __block id objectForKey = nil;

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    [self objectForKey:key block:^(TMMemoryCache *cache, NSString *key, id object) {
        objectForKey = object;
        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    #if !OS_OBJECT_USE_OBJC
    dispatch_release(semaphore);
    #endif

    return objectForKey;
}

TMCache在同步接口裏面經過信號量來阻塞當前線程,而後切換到其餘線程(具體代碼在其異步接口裏面,是經過dispatch_async到一個並行隊列來實現的)去執行讀取操做。按照yy的說法主要的性能損耗應該在這個線程切換操做,同步接口不必去切換線程執行。

yy這邊的思路是經過自旋鎖來保證線程安全,但仍然在當前線程去執行讀操做,這樣就能夠節省線程切換帶來的開銷。(不過我在YYCache的最新代碼裏看到的是普通的互斥鎖,並無用自旋鎖,應該是後面又作了方案上的修改?)

除了加鎖串行,dispatch_sync實現同步的方案是否可行呢?

除了yy提供的加鎖串行方案,咱們來看看前面介紹過的barrier並行讀串行寫方案是否也存在性能問題。若是使用該方案,同步接口多是這樣的:

- (id)objectForKey:(NSString *)key
{
        __block id object = nil;
        dispatch_sync(concurrent_queue, ^{
                object = cache[key];                                            // 讀接口,不用barrier,保證讀與讀可以並行
        });
        return object;
}

- (void)setObject:(id)object forKey:(NSString *)key
{
        dispatch_barrier_sync(concurrent_queue, ^{    // 寫接口,barrier保證與讀互斥
                cache[key] = object;
        });
}

通過demo驗證,能夠發現雖然是dipatch到一個concurrent_queue中執行,可是因爲是sync同步派發,實際上並不會切換到新的線程執行。也就是說該方案也能作到節省線程切換的開銷。

劃重點: dispatch_sync不會切換調用線程執行,這個結論好像也是個面試考點?

那麼該方案與加鎖串行的方案相比,性能如何呢?

barrier實現並行讀串行寫 vs 互斥鎖串行處理的性能比較

單線程測試

首先跑了下YYCache自帶的benchmark,其原理是測試單線程作20000次讀或者寫的總耗時。其中TMCache new表示修改成dispatch_sync後的測試數據。

===========================
Memory cache set 200000 key-value pairs
NSDictionary:      67.53
NSDict+Lock:       73.47
YYMemoryCache:    133.08
PINMemoryCache:   257.59
NSCache:          457.63
TMCache:         7638.25
TMCache new:      297.58

===========================
Memory cache get 200000 key-value pairs
NSDictionary:      43.32
NSDict+Lock:       53.68
YYMemoryCache:     93.15
PINMemoryCache:   141.12
NSCache:           73.89
TMCache:         7446.88
TMCache new:      210.80

從結論看,單線程用dispatch_sync的方案,比YYCache的鎖串行方案要慢2倍多一點,比原始的信號量強行同步操做要快25到35倍。

因此開發過程當中須要避免相似TMCache原始寫法的同步接口實現方案。

多線程測試

display_barrier是並行讀,串行寫的方案,理論上在多線程併發的場景會更有優點,因此我嘗試寫了個多線程的benchmark來對比性能,代碼以下:

typedef void(^exec_block)(id key, id value);
+ (void)benchmark:(NSString *)type exec:(exec_block)block keys:(NSArray *)keys values:(NSArray *)values
{
    int count = 10000;
    printf("Memory cache %s %i pairs\n", type.UTF8String, count);
    __block NSTimeInterval begin, end, time;

    begin = CACurrentMediaTime();
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create(type.UTF8String, DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < count; i++) {
        dispatch_group_async(group, queue, ^{
            block(keys[i], values[i]);  // 執行不一樣cache的具體set或者get操做
        });
    }

    dispatch_group_notify(group, queue, ^{
        end = CACurrentMediaTime();
        time = end - begin;
        printf("%s:   %8.2f\n", type.UTF8String, time * 1000);
    });
}

由於是併發執行,因此結束時間是經過dispatch_group來拿的。函數接收外部傳入的exec_block做爲輸入,block內部執行具體的YYCacheTMCache的set/get方法。

這個測試方案存在一個問題,整個耗時大頭在dispatch_group_async的派發上,block內部是否執行cache的get/set方法,對總體耗時結果影響不大。因此最終我也沒有獲得一個比較準確的測試結果,或許固定建立幾個線程來作併發測試會更靠譜一些。

高性能的本地存儲方案

除了多線程的高性能實現,YYCache在本地持久化如何提升性能也有個小策略。核心問題應該就是二進制數據從文件讀寫和從sqlite讀寫究竟哪一個更快?sqlite官網有一個測試結論

image-20200608145630124.png

表格中數值表示存文件耗時除以存數據庫耗時,大於1表示存數據庫更快,表示爲綠色。

基於這個結論和本身的實測結果,YYCache採起的方案是大於20k的採起直接存儲文件,而後在sqlite裏面存元信息(好比說文件路徑),小於20k的直接存儲到sqlite裏面。

數據完整性保障:

對於有關聯的數據,存儲時必定須要保障其完整性,要麼全成功,要麼全失敗。好比YYCache在存儲文件時,存在數據庫的元信息和實際文件的存儲就必須保障原子性。若是雲信息存儲成功,可是文件存儲失敗,就會致使邏輯問題。具體YYCache代碼以下:

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;

這裏能夠看到,只有文件存成功了纔會存數據庫元信息,若是數據庫元信息存失敗了,會去刪除已經存儲成功的文件。

咱們業務開發存儲關聯數據的時候,也須要注意這個邏輯。

緩存淘汰策略

除了性能以外,YYCache也新增了一些實用功能。

好比LRU算法,基於存儲時長、數量、大小的緩存控制策略等。

LRU算法採用經典的雙鏈表+哈希表的方案實現的,很適合不熟悉的同窗參考學習,這裏就不展開了。


原文連接: http://www.luoyibu.cn/posts/1...

歡迎掃碼關注個人微信公衆號
image

相關文章
相關標籤/搜索