今年上半年時候看到微信開發團隊的這麼一篇文章MMKV--基於 mmap 的 iOS 高性能通用 key-value 組件,文中提到了用mmap實現一個高性能KV組件,雖然並無展現太多的具體代碼,可是基本思路講的仍是很清楚的。
文章最後提到了開源計劃,等了快半年還沒看到這個組件源碼,因而決定本身試着寫一個。python
按照慣例先上輪子,能夠給個star收藏一下哦~git
FastKV githubgithub
在開始寫這個組件以前,應該先調研一下NSUserDefaults性能(ps:這裏有個失誤,事實上我是在寫完這個組件之後才調研的)。objective-c
據我所知NSUserDefaults有一層內存緩存的,因此它提供了一個叫synchronize
的方法用於同步磁盤和緩存,可是這個方法如今蘋果在文檔中告訴咱們for any other reason: remove the synchronize call
,總之就是不再須要調用這個方法了。緩存
測試結果以下(寫入1w次,值類型是NSInteger,環境:iPhone 8 64G, iOS 11.4)微信
非synchronize
耗時:137ms微信開發
synchronize
耗時:3758msapp
很明顯synchronize
對性能的損耗很是大,由於本文須要的是一個高性能、高實時性的key-value持久化組件,也就是說在一些極端狀況下數據也須要可以被持久化,同時又不影響性能。所謂極端狀況,好比說在App發生Crash的時候數據也可以被存儲到磁盤中,並不會由於緩存和磁盤沒來得及同步而形成數據丟失。框架
從數據上咱們能夠看到非synchronize
下的性能仍是挺好的,比上面那篇微信的文章中的測試結果貌似要好不少嘛。那麼mmap
和NSUserDefaults
在高性能上的優點彷佛並不明顯的。模塊化
那麼咱們再來看一下高實時性這個方面。既然蘋果在文檔中告訴咱們remove the synchronize
,難道蘋果已經解決的NSUserDefaults
的高實時性和高性能兼顧的問題?抱着試一試的心態筆者作了一下測試,答案是否認的。在不使用synchronize
的狀況下,極端狀況依舊會出現數據丟失的問題。那麼咱們的mmap
仍是有它的用武之地的,至少它在保證的高實時性的時候還兼顧到了性能問題。
爲了便於更好的理解,在閱讀接下來的部分前請先閱讀這篇文章。MMKV--基於 mmap 的 iOS 高性能通用 key-value 組件
具體的實現筆者仍是參考了上面微信團隊的MMKV,那篇文章已經講得比較詳細了,所以對那篇文章的分析在這裏就再也不展開了。
在這裏要提到的一個點是有關於數據序列化。MMKV在序列化時使用了Google開源的protobuf
,筆者在實現的時候考慮到各方面緣由決定自定義一個內存數據格式,這樣就避免了對protobuf
的依賴。
自定義協議主要分爲3個部分:Header Segment、Data Segment、Check Code。
Header Segment:
32/64bit | 32bit | 32/64bit | 32/64bit | 32/64bit |
---|---|---|---|---|
VALUE_TYPE | VERSION | OBJC_TYPE length | KEY length | DATA length |
這部分的長度是固定的,160bit或288bit。
VALUE_TYPE
:數據的類型,目前有8種類型bool、nil、int3二、int6四、float、double、string、data。
VERSION
:數據記錄時的版本。
OBJC_TYPE length
:OC類名字符串的長度。
KEY length
:key的長度。
DATA length
:value的長度。
Data Segment:
Data | Data | Data |
---|---|---|
OBJC_TYPE | KEY | DATA |
OBJC_TYPE
:OC類名的字符串。
KEY
:key。
DATA
:value。
Check Code:
16bit |
---|
CRC code |
CRC code:倒數16位以前數據的CRC-16循環冗餘檢測碼,用於後期數據校驗。
mmap的使用涉及一個內存空間的分配問題,咱們在這裏提供了兩種內存分配策略。
一種策略是在MMKV的文章中提到,在append時遇到內存不夠用的時候,會進行序列化排重。在序列化排重後仍是不夠用的話就將文件擴大一倍,直到夠用。
size_t allocationSize = 1;
while (allocationSize <= neededSize) {
allocationSize *= 2;
}
return allocationSize;
複製代碼
另外一種策略參考了python list的內存分配實現。
size_t allocationSize = (neededSize >> 3) + (neededSize < 9 ? 3 : 6);
return allocationSize + neededSize;
複製代碼
在只考慮在添加新的key的狀況下這兩種內存分配策略比較好的,可是在屢次更新key時可能會出現連續的排重操做,下面用一個例子來講明。
若是當前分配的mmap size
僅僅只比當前正在使用的size多出極少極少一點,以致於接下來任何的append操做都會觸發排重,可是因爲每次都是對key進行更新操做,若是當前mmap的數據已是最小集合了(沒有任何重複key的數據),因而在排重完成後mmap size
又恰好夠用,不須要從新分配mmap size
。這時候mmap size
又是僅僅只比當前正在使用的size多出極少極少一點,而後任何的append又會走一遍上述邏輯。
爲了解決這個問題,筆者在append操做的時候附加了一個邏輯:正常狀況下allocationSize
是按照當前實際neededSize
來計算的,若是當前是對key進行更新操做,那麼計算allocationSize
會迭代兩次,即第一次計算的allocationSize
就是第二次計算中的neededSize
。
size_t totalSize = dataLength + FastKVHeaderSize;
size_t neededSize = updated ? [self _fkvAllocationSizeWithNeededSize:totalSize + size] : totalSize + size;
if (neededSize > _mmsize
|| (updated && [self _fkvAllocationSizeWithNeededSize:neededSize] > _mmsize)) {
// 從新分配mmap
}
複製代碼
有一些OC對象的存儲是能夠優化的,好比NSDate、NSURL,在實際存儲時能夠當成double和NSString來進行序列化,既提升了性能又減小了空間的佔用。
測試結果以下(1w次,值類型是NSInteger,環境:iPhone 8 64G, iOS 11.4)
add耗時:70ms (NSUserDefults Sync:3469ms)
update耗時:80ms (NSUserDefults Sync:3521ms)
get耗時:10ms (NSUserDefults:48ms)
測試下來mmap性能確實比NSUserDefults Sync
要好很多,也和微信那篇文章中對MMKV的性能測試結果基本一致。總的來講,若是對實時性要求不高的項目,建議仍是使用官方的NSUserDefults
。
WhiteElephantKiller —無用代碼掃描工具 github