今年上半年时候看到微信开发团队的这么一篇文章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