iOS的高性能、高實時性key-value持久化組件

今年上半年時候看到微信開發團隊的這麼一篇文章 MMKV--基於 mmap 的 iOS 高性能通用 key-value 組件,文中提到了用mmap實現一個高性能KV組件,雖然並無展現太多的具體代碼,可是基本思路講的仍是很清楚的。
文章最後提到了開源計劃,等了快半年還沒看到這個組件源碼,因而決定本身試着寫一個。

輪子

按照慣例先上輪子,能夠先給個小星星哦~git

FastKV githubgithub

關於NSUserDefaults

在開始寫這個組件以前,應該先調研一下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下的性能仍是挺好的,比上面那篇微信的文章中的測試結果貌似要好不少嘛。那麼mmapNSUserDefaults在高性能上的優點彷佛並不明顯的。框架

那麼咱們再來看一下高實時性這個方面。既然蘋果在文檔中告訴咱們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

其餘開源做品

TinyPart —模塊化框架 github 思否

Coolog —可擴展的log框架 github 思否

WhiteElephantKiller —無用代碼掃描工具 github

相關文章
相關標籤/搜索