MMKV浅析

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为自动保存,无需手动调用同步。

参考链接:

MMKV--基于 mmap 的 iOS 高性能通用 key-value 组件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352