iOS的高性能、高实时性key-value持久化组件

栏目: IOS · 发布时间: 5年前

内容简介:按照惯例先上轮子,可以给个star收藏一下哦~

今年上半年时候看到微信开发团队的这么一篇文章 MMKV--基于 mmap 的 iOS 高性能通用 key-value 组件 ,文中提到了用mmap实现一个高性能KV组件,虽然并没有展示太多的具体代码,但是基本思路讲的还是很清楚的。文章最后提到了开源计划,等了快半年还没看到这个组件源码,于是决定自己试着写一个。

轮子

按照惯例先上轮子,可以给个star收藏一下哦~

FastKV github

关于NSUserDefaults

在开始写这个组件之前,应该先调研一下NSUserDefaults性能(ps:这里有个失误,事实上我是在写完这个组件以后才调研的)。

据我所知NSUserDefaults有一层内存缓存的,所以它提供了一个叫synchronize的方法用于同步磁盘和缓存,但是这个方法现在苹果在文档中告诉我们for any other reason: remove the synchronize call,总之就是再也不需要调用这个方法了。

测试结果如下(写入1w次,值类型是NSInteger,环境:iPhone 8 64G, iOS 11.4)

非synchronize耗时: 137ms

synchronize耗时: 3758ms

很明显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:

iOS的高性能、高实时性key-value持久化组件

这部分的长度是固定的,160bit或288bit。

  • VALUE_TYPE:数据的类型,目前有8种类型bool、nil、int32、int64、float、double、string、data。

  • VERSION:数据记录时的版本。

  • OBJC_TYPE length:OC类名字符串的长度。

  • KEY length:key的长度。

  • DATA length:value的长度。

Data Segment:

iOS的高性能、高实时性key-value持久化组件

  • OBJC_TYPE:OC类名的字符串。

  • KEY:key。

  • DATA:value。

Check Code:

iOS的高性能、高实时性key-value持久化组件

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。

作者:RyanLeeLY

链接:https://juejin.im/post/5b7e9bbc51882542e32a9c4d


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Visual C++网络通信协议分析与应用实现

Visual C++网络通信协议分析与应用实现

汪晓平、钟军 / 人民邮电出版社 / 2003-2-1 / 60.00元

本书介绍了如何利用Visual一起来看看 《Visual C++网络通信协议分析与应用实现》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试