MMKV浅析
MMKV 是微信开源的一个基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。微信团队为了发现记录特殊文字引起微信 iOS 系统的 crash,在关键代码前后进行计数器的加减,通过检查计数器的异常,来发现引起闪退的异常文字,但同时因为诸多cell的复杂页面情境下希望新加的计时器不会影响性能,另外这些计数器需要永久存储下来——因为闪退随时可能发生,所以亟需高性能的通用 key-value 存储组件,而微信团队在实时写入和高性能的选择标准下,通过对比NSUserDefaults、SQLite 等常见组件,最终选择了mmap 内存映射文件,并将其封装成为了MMKV组件。下面我们来逐步进行了解
1.mmap简介
认真分析mmap:是什么 为什么 怎么用这篇文章讲的炒鸡详细,很佩服作者,本人也不想ctrl+c/v一遍,所以直接附上链接入口,看官可自行查阅。但在此处总结下:mmap实现了一种使用内存映射到磁盘文件的方法,将本该属于磁盘文件的对象映射到了进程地址空间中,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动(默认并不实时)回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数,对文件直接通过内存映射读取从而跨过了页缓存,减少数据拷贝次数,用内存读写取代I/O读写,提高文件读取效率。另外,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享,从而达到进程间通信和进程间共享的目的。简言之,很强大。
2.protobuf
在数据序列化方面微信团队选用了protobuf 协议,出于通用化的考虑将多样化的 value 通过 protobuf 协议序列化成统一的内存块(buffer),然后再进行相应存储。
2.1 protobuf是什么
无处不在的阅读理解【doge脸】
Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. You can even update your data structure without breaking deployed programs that are compiled against the "old" format.
protobuf是一种灵活高效的序列化结构机制,就像xml,但是protobuf更轻量、更快并且更简单。一旦你限定了你想要的数据结构,那么你就可以使用特殊的构建代码实现对大量数据结构的读写,并且支持多种语言哦~你甚至可以更新你的数据构建,哪怕新的数据结构与老的完全相反,这丝毫不影响已经部署完成的程序。
也就是说protobuf帮我们轻松实现了序列化和反序列化,即使变更数据结构,也不会产生太大的影响,这对于数据结构多变的实际业务场景来说简直太有必要了。
3.写入优化&空间增长
因为标准 protobuf并 不提供增量更新的能力,每次写入都必须全量写入。查看代码我们也能看到最底层调用的方法是使用的append而非直接替换:
- (BOOL)setRawData:(NSData *)data forKey:(NSString *)key {
if (data.length <= 0 || key.length <= 0) {
return NO;
}
CScopedLock lock(m_lock);
[m_dic setObject:data forKey:key];
m_hasFullWriteBack = NO;
return [self appendData:data forKey:key];
}
但是这样就会引发两个问题:
1.很大程度上可能存在相同key但是存储了多个不同的value。
2.不断 append 的话,文件大小会增长得不可控。
针对这两个问题的处理方式是:
1.在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的。
2.对于空间增长的问题:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。所以在每次append之前都会先调用- (BOOL)ensureMemorySize:(size_t)newSize;方法检查一下是否有足够空间,如果没有则按照每次2倍的大小去扩展空间:
- (BOOL)ensureMemorySize:(size_t)newSize {
...
if (newSize >= m_output->spaceLeft()) {
// try a full rewrite to make space
static const int offset = pbFixed32Size(0);
NSData *data = [MiniPBCoder encodeDataWithObject:m_dic];
size_t lenNeeded = data.length + offset + newSize;
size_t avgItemSize = lenNeeded / std::max<size_t>(1, m_dic.count);
size_t futureUsage = avgItemSize * std::max<size_t>(8, m_dic.count / 2);
// 1. no space for a full rewrite, double it
// 2. or space is not large enough for future usage, double it to avoid frequently full rewrite
if (lenNeeded >= m_size || (lenNeeded + futureUsage) >= m_size) {
size_t oldSize = m_size;
do {
m_size *= 2;
} while (lenNeeded + futureUsage >= m_size);
...
}
...
}
...
}
3.另外针对空间增长,mmkv还提供了- (void)trim;方法来提供了通过手动调用减小多余占用内存的功能,正如每次扩增时按2倍扩增,缩减时也是每次除以2:
- (void)trim {
...
auto oldSize = m_size;
while (m_size > (m_actualSize * 2)) {
m_size /= 2;
}
...
}
4.crc 校验
微信团队考虑到文件系统、操作系统都有一定的不稳定性,另外增加了 crc 校验,对无效数据进行甄别,根据微信提供的数据:在 iOS 微信现网环境上,观察到有平均约 70w 日次的数据校验不通过。
4.1 crc 校验简介
CRC即循环冗余校验码(Cyclic Redundancy Check):是数据通信领域中最常用的一种查错校验码,其特征是信息字段和校验字段的长度可以任意选定。循环冗余检查(CRC)是一种数据传输检错功能,对数据进行多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。也就是接受方和发送方约定一个用来计算的二进制数(比如x),在整个传输过程中,这个数始终保持不变。循环冗余校验码(CRC)的基本原理是:在K位信息码后再拼接R位的校验码,整个编码长度为N位,因此,这种编码也叫(N,K)码。那么发送方发送时根据约定的x计算出要补全在K位信息码后的R位校验码,然后发送,接收方接收到数据之后通过约定好的x对收到的数据进行校验,即可查验在数据传输过程中有否出错。
5.MMKV 使用
mmkv类提供了一个单例访问mmkv对象defaultMMKV,或者也可以通过指定mmapID和cryptKey生成专属的、使用指定cryptKey加密的mmkv对象,另外提供的api也清晰易懂:
MMKV *mmkv = [MMKV mmkvWithID:@"test/case1"];
[mmkv setBool:YES forKey:@"bool"];
NSLog(@"bool:%d", [mmkv getBoolForKey:@"bool"]);
[mmkv setInt32:-1024 forKey:@"int32"];
NSLog(@"int32:%d", [mmkv getInt32ForKey:@"int32"]);
[mmkv setUInt32:std::numeric_limits<uint32_t>::max() forKey:@"uint32"];
NSLog(@"uint32:%u", [mmkv getUInt32ForKey:@"uint32"]);
[mmkv setInt64:std::numeric_limits<int64_t>::min() forKey:@"int64"];
NSLog(@"int64:%lld", [mmkv getInt64ForKey:@"int64"]);
[mmkv setUInt64:std::numeric_limits<uint64_t>::max() forKey:@"uint64"];
NSLog(@"uint64:%llu", [mmkv getInt64ForKey:@"uint64"]);
[mmkv setFloat:-3.1415926 forKey:@"float"];
NSLog(@"float:%f", [mmkv getFloatForKey:@"float"]);
[mmkv setDouble:std::numeric_limits<double>::max() forKey:@"double"];
NSLog(@"double:%f", [mmkv getDoubleForKey:@"double"]);
[mmkv setString:@"hello, mmkv" forKey:@"string"];
NSLog(@"string:%@", [mmkv getStringForKey:@"string"]);
[mmkv setObject:nil forKey:@"string"];
NSLog(@"string after set nil:%@, containsKey:%d",
[mmkv getObjectOfClass:NSString.class
forKey:@"string"],
[mmkv containsKey:@"string"]);
[mmkv setDate:[NSDate date] forKey:@"date"];
NSLog(@"date:%@", [mmkv getDateForKey:@"date"]);
[mmkv setData:[@"hello, mmkv again and again" dataUsingEncoding:NSUTF8StringEncoding] forKey:@"data"];
NSData *data = [mmkv getDataForKey:@"data"];
NSLog(@"data:%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
// 当然也可以通过remov方法移除某一对key-value
[mmkv removeValueForKey:@"bool"];
NSLog(@"bool:%d", [mmkv getBoolForKey:@"bool"]);
[mmkv close];
5.1支持的数据类型
【1】支持以下 C 语语言基础类型:
bool、int32、int64、uint32、uint64、float、double
【2】支持以下 ObjC 类型:
NSString、NSData、NSDate
5.2性能比较
官方提供的将 MMKV、NSUserDefaults 的性能进行对比(循环写入1w 次数据,测试环境:iPhone X 256G, iOS 11.2.6,单位:ms),比较结果如下所示:
另外相比较NSUserDefaults还需要手动调用synchronize保存来说,MMKV为自动保存,无需手动调用同步。
参考链接: