簡單列舉一下,iOS的本地緩存方案有挺多,各有各的適用場景:html
NSUserDefault
: 系統提供的最簡便的key-value本地存儲方案,適合比較輕量的數據存儲,好比一些業務flag。主要緣由仍是其底層是用plist文件存儲的,在數據量逐步變大後,可能會發生性能問題。面試
不管是本身轉換業務數據爲二進制再writeFile,仍是直接利用系統的NSKeyedArchiver
接口歸檔成文件,都屬於文件存儲的方案。優點是開發簡單,業務能夠自行控制單文件的存儲內容以免可能發生的性能問題。算法
底層利用到數據的存儲方案,比較適用數據量大,有查詢,排序等需求的存儲場景,缺點就是開發略複雜一些。sql
CoreData感受好像應用並非很普遍?數據庫
這裏特指提供Key-Value形式接口的緩存庫,底層緩存可能使用文件或者sqlite都有。本文討論的YYCache
底層是混合使用文件+sqlite的存儲方式。基於接口簡便,性能優於NSUserDefault
的特性,應該適用於大多數的業務場景,可是沒法適用上面數據庫相似的使用場景。緩存
這裏其實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的最新代碼裏看到的是普通的互斥鎖,並無用自旋鎖,應該是後面又作了方案上的修改?)
除了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
不會切換調用線程執行,這個結論好像也是個面試考點?
那麼該方案與加鎖串行的方案相比,性能如何呢?
單線程測試
首先跑了下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內部執行具體的YYCache
和TMCache
的set/get方法。
這個測試方案存在一個問題,整個耗時大頭在dispatch_group_async
的派發上,block內部是否執行cache的get/set方法,對總體耗時結果影響不大。因此最終我也沒有獲得一個比較準確的測試結果,或許固定建立幾個線程來作併發測試會更靠譜一些。
除了多線程的高性能實現,YYCache
在本地持久化如何提升性能也有個小策略。核心問題應該就是二進制數據從文件讀寫和從sqlite讀寫究竟哪一個更快?sqlite官網有一個測試結論:
表格中數值表示存文件耗時除以存數據庫耗時,大於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...
歡迎掃碼關注個人微信公衆號