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

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

輪子

按照慣例先上輪子,能夠給個star收藏一下哦~git

FastKV githubgithub

關於NSUserDefaults

在開始寫這個組件以前,應該先調研一下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下的性能仍是挺好的,比上面那篇微信的文章中的測試結果貌似要好不少嘛。那麼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循環冗餘檢測碼,用於後期數據校驗。

空間增加

分配策略

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

其餘開源做品

TinyPart —模塊化框架 github 掘金

Coolog —可擴展的log框架 github 掘金

WhiteElephantKiller —無用代碼掃描工具 github

參考

MMKV--基於 mmap 的 iOS 高性能通用 key-value 組件

alexlee002/mmkv

相關文章
相關標籤/搜索