今年上半年時候看到微信開發團隊的這麼一篇文章 MMKV--基於 mmap 的 iOS 高性能通用 key-value 組件,文中提到了用mmap實現一個高性能KV組件,雖然並無展現太多的具體代碼,可是基本思路講的仍是很清楚的。
文章最後提到了開源計劃,等了快半年還沒看到這個組件源碼,因而決定本身試着寫一個。
按照慣例先上輪子,能夠先給個小星星哦~git
FastKV githubgithub
在開始寫這個組件以前,應該先調研一下NSUserDefaults性能(ps:這裏有個失誤,事實上我是在寫完這個組件之後才調研的)。objective-c
據我所知NSUserDefaults有一層內存緩存的,因此它提供了一個叫synchronize
的方法用於同步磁盤和緩存,可是這個方法如今蘋果在文檔中告訴咱們for any other reason: remove the synchronize call
,總之就是不再須要調用這個方法了。segmentfault
測試結果以下(寫入1w次,值類型是NSInteger,環境:iPhone 8 64G, iOS 11.4)緩存
非synchronize
耗時:137ms微信
synchronize
耗時:3758ms微信開發
很明顯synchronize
對性能的損耗很是大,由於本文須要的是一個高性能、高實時性的key-value持久化組件,也就是說在一些極端狀況下數據也須要可以被持久化,同時又不影響性能。所謂極端狀況,好比說在App發生Crash的時候數據也可以被存儲到磁盤中,並不會由於緩存和磁盤沒來得及同步而形成數據丟失。app
從數據上咱們能夠看到非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循環冗餘檢測碼,用於後期數據校驗。
在MMKV的文章中提到,在append時遇到內存不夠用的時候,會進行序列化排重;在序列化排重後仍是不夠用的話就將文件擴大一倍,直到夠用。
在只考慮在添加新的key的狀況下這確實是一種簡單有效的內存分配策略,可是在屢次更新key時可能會出現連續的排重操做,下面用一個例子來講明。
若是當前分配的mmap size
僅僅只比當前正在使用的size多出極少極少一點,以致於接下來任何的append操做都會觸發排重,可是因爲每次都是對key進行更新操做,若是當前mmap的數據已是最小集合了(沒有任何重複key的數據),因而在排重完成後mmap size
又恰好夠用,不須要從新分配mmap size
。這時候mmap size
又是僅僅只比當前正在使用的size多出極少極少一點,而後任何的append又會走一遍上述邏輯。
爲了解決這個問題,筆者在append操做的時候附加了一個邏輯:若是當前是對key進行更新操做,那麼從新分配mmap size
的需求大小將會擴大1倍。也就是說若是對key進行更新操做後觸發排重,這時mmap size
的將會按當前需求2倍的大小嚐試進行從新分配,以空間來換取時間性能。
if (data.length + _cursize >= _mmsize) { // 若是是對key是update操做,那麼就按照真實需求大小2倍的來嘗試進行從新分配。 [self reallocWithExtraSize:data.length scale:isUpdated?2:1]; } else { memcpy((char *)_mmptr + _cursize, data.bytes, data.length); _cursize += data.length; uint64_t dataLength = _cursize - FastKVHeaderSize; memcpy((char *)_mmptr + sizeof(uint32_t) + [FastKVMarkString lengthOfBytesUsingEncoding:NSUTF8StringEncoding], &dataLength, 8); }
有一些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