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

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

内容简介:按照惯例先上轮子,可以给个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


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

查看所有标签

猜你喜欢:

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

Numerical Recipes 3rd Edition

Numerical Recipes 3rd Edition

William H. Press、Saul A. Teukolsky、William T. Vetterling、Brian P. Flannery / Cambridge University Press / 2007-9-6 / GBP 64.99

Do you want easy access to the latest methods in scientific computing? This greatly expanded third edition of Numerical Recipes has it, with wider coverage than ever before, many new, expanded and upd......一起来看看 《Numerical Recipes 3rd Edition》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具