YYCache 設計思路
iOS 開發中總會用到各種快取,最初我是用的一些開源的快取庫,但到總覺得缺少某些功能,或某些 API 設計的不夠好用。YYCache ( https://github.com/ibireme/YYCache ) 是我新造的一個輪子,下面說一下這個輪子的設計思路。
記憶體快取
通常一個快取是由記憶體快取和磁碟快取組成,記憶體快取提供容量小但高速的存取功能,磁碟快取提供大容量但低速的持久化儲存。相對於磁碟快取來說,記憶體快取的設計要更簡單些,下面是我調查的一些常見的記憶體快取。
NSCache 是蘋果提供的一個簡單的記憶體快取,它有著和 NSDictionary 類似的 API,不同點是它是執行緒安全的,並且不會 retain key。我在測試時發現了它的幾個特點:NSCache 底層並沒有用 NSDictionary 等已有的類,而是直接呼叫了 libcache.dylib,其中執行緒安全是由 pthread_mutex 完成的。另外,它的效能和 key 的相似度有關,如果有大量相似的 key (比如 "1", "2", "3", ...),NSCache 的存取效能會下降得非常厲害,大量的時間被消耗在 CFStringEqual() 上,不知這是不是 NSCache 本身設計的缺陷。
TMMemoryCache 是 TMCache 的記憶體快取實現,最初由 Tumblr 開發,但現在已經不再維護了。TMMemoryCache 實現有很多 NSCache 並沒有提供的功能,比如數量限制、總容量限制、存活時間限制、記憶體警告或應用退到後臺時清空快取等。TMMemoryCache 在設計時,主要目標是執行緒安全,它把所有讀寫操作都放到了同一個 serial queue 中,然後用 dispatch_semaphore 來保證最多隻有一個執行緒訪問 queue。它錯誤的用了大量非同步 block 回撥來實現存取功能,以至於產生了很大的效能和死鎖問題。
PINMemoryCache 是 Tumblr 宣佈不在維護 TMCache 後,由 Pinterest 維護和改進的一個記憶體快取。它的功能和介面基本和 TMMemoryCache 一樣,但修復了效能和死鎖的問題。它同樣也用 dispatch_semaphore 來保證執行緒安全,但去掉了 serial queue,避免了執行緒切換帶來的巨大開銷,也避免了可能的死鎖。
YYMemoryCache 是我開發的一個記憶體快取,相對於 PINMemoryCache 來說,我去掉了非同步訪問的介面,儘量優化了同步訪問的效能,用 OSSpinLock 來保證執行緒安全。另外,快取內部用雙向連結串列和 NSDictionary 實現了 LRU 淘汰演算法,相對於上面幾個算是一點進步吧。
下面的單執行緒的 Memory Cache 效能基準測試:
可以看到 YYMemoryCache 的效能不錯,僅次於 NSDictionary + OSSpinLock;
NSCache 的寫入效能稍差,讀取效能不錯;
PINMemoryCache 的讀寫效能也還可以,但讀取速度差於 NSCache;
TMMemoryCache 效能太差以至於圖上都看不出來了。
磁碟快取
為了設計一個比較好的磁碟快取,我調查了大量的開源庫,包括 TMDiskCache、PINDiskCache、SDWebImage、FastImageCache 等,也調查了一些閉源的實現,包括 NSURLCache、Facebook 的 FBDiskCache 等。他們的實現技術大致分為三類:基於檔案讀寫、基於 mmap 檔案記憶體對映、基於資料庫。
TMDiskCache, PINDiskCache, SDWebImage 等快取,都是基於檔案系統的,即一個 Value 對應一個檔案,通過檔案讀寫來快取資料。他們的實現都比較簡單,效能也都相近,缺點也是同樣的:不方便擴充套件、沒有元資料、難以實現較好的淘汰演算法、資料統計緩慢。
FastImageCache 採用的是 mmap 將檔案對映到記憶體。用過 MongoDB 的人應該很熟悉 mmap 的缺陷:熱資料的檔案不要超過實體記憶體大小,不然 mmap 會導致記憶體交換嚴重降低效能;另外記憶體中的資料是定時 flush 到檔案的,如果資料還未同步時程式掛掉,就會導致資料錯誤。拋開這些缺陷來說,mmap 效能非常高。
NSURLCache、FBDiskCache 都是基於 SQLite 資料庫的。基於資料庫的快取可以很好的支援元資料、擴充套件方便、資料統計速度快,也很容易實現 LRU 或其他淘汰演算法,唯一不確定的就是資料庫讀寫的效能,為此我評測了一下 SQLite 在真機上的表現。iPhone 6 64G 下,SQLite 寫入效能比直接寫檔案要高,但讀取效能取決於資料大小:當單條資料小於 20K 時,資料越小 SQLite 讀取效能越高;單條資料大於 20K 時,直接寫為檔案速度會更快一些。這和 SQLite 官網的描述 基本一致。另外,直接從官網下載最新的 SQLite 原始碼編譯,會比 iOS 系統自帶的 sqlite3.dylib 效能要高很多。基於 SQLite 的這種表現,磁碟快取最好是把 SQLite 和檔案儲存結合起來:key-value 元資料儲存在 SQLite 中,而 value 資料則根據大小不同選擇 SQLite 或檔案儲存。NSURLCache 選定的資料大小的閾值是 16K;FBDiskCache 則把所有 value 資料都儲存成了檔案。
我的 YYDiskCache 也是採用的 SQLite 配合檔案的儲存方式,在 iPhone 6 64G 上的效能基準測試結果見下圖。在存取小資料 (NSNumber) 時,YYDiskCache 的效能遠遠高出基於檔案儲存的庫;而較大資料的存取效能則比較接近了。但得益於 SQLite 儲存的元資料,YYDiskCache 實現了 LRU 淘汰演算法、更快的資料統計,更多的容量控制選項。
備註:
關於鎖:
OSSpinLock 自旋鎖,效能最高的鎖。原理很簡單,就是一直 do while 忙等。它的缺點是當等待時會消耗大量 CPU 資源,所以它不適用於較長時間的任務。對於記憶體快取的存取來說,它非常合適。
dispatch_semaphore 是訊號量,但當訊號總量設為 1 時也可以當作鎖來。在沒有等待情況出現時,它的效能比 pthread_mutex 還要高,但一旦有等待情況出現時,效能就會下降許多。相對於 OSSpinLock 來說,它的優勢在於等待時不會消耗 CPU 資源。對磁碟快取來說,它比較合適。
關於 Realm:
Realm 是一個比較新的資料庫,號稱是針對移動應用所設計。我在測試 SQLite 效能時,也嘗試對它做了些簡單的評測。我從 Realm 官網下載了它提供的 benchmark 專案,更新 SQLite 到官網最新的版本,並啟用了 SQLite 的 sqlite3_stmt 快取。評測結果顯示 Realm 在寫入效能上差 SQLite 很多,讀取小資料時也差 SQLite 不少,只有讀取較大資料時 Realm 才有很大的優勢。我想看看它的實現原理,但發現 Realm 的核心 realm-core 是閉源的(還有跡象未來要收費),能知道的是 Realm 應該用 了 mmap 把檔案對映到記憶體,所以才在較大資料讀取時獲得很高的效能。另外我注意到添加了 Realm 的 App 會在啟動時向某幾個 IP 傳送資料,所以我強烈建議大家不要用 Realm。
覺得文章有用?立即:和朋友一起 共學習 共進步!
建議繼續學習:
- Buffer和cache的區別是什麼? (閱讀:6101)
- Linux作業系統中記憶體buffer和cache的區別 (閱讀:4681)
- 學習:一個併發的Cache (閱讀:4341)
- 關於Linux的檔案系統cache (閱讀:4160)
- Twitter架構圖(cache篇) (閱讀:3740)
- 詳解MyISAM Key Cache(前篇) (閱讀:3600)
- [squid] 過期時間在 60 秒內 squid 不 Cache 的問題 (閱讀:3440)
- 7個示例科普CPU Cache (閱讀:3340)
- 為什麼程式設計師需要關心順序一致性(Sequential Consistency)而不是Cache一致性(Cache Coherence?) (閱讀:3030)
- Oracle11g中的result cache (閱讀:2762)
QQ技術交流群:445447336,歡迎加入!
掃一掃訂閱我的微訊號:IT技術部落格大學習