MMKV概览

1 MMKV概览

1.1 什么是MMKV

引自 github.com/Tencent/MMKV 介绍[https://github.com/Tencent/MMKV/blob/master/readme_cn.md]

MMKV——基于 mmap 的高性能通用 key-value 组件
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能 高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移 植到 Android / macOS / Windows 平台,一并开源。

MMKV 源起
在微信客户端的日常运营中,时不时就会爆发特殊文字引起系统的 crash,文章里面设计的技术方 案是在关键代码前后进行计数器的加减,通过检查计数器的异常,来发现引起闪退的异常文字。 在会话列表、会话界面等有大量 cell 的地方,希望新加的计时器不会影响滑动性能;另外这些计 数器还要永久存储下来——因为闪退随时可能发生。这就需要一个性能非常高的通用 key-value 存 储组件,我们考察了 SharedPreferences、NSUserDefaults、SQLite 等常见组件,发现都没能满足如 此苛刻的性能要求。考虑到这个防 crash 方案最主要的诉求还是实时写入,而 mmap 内存映射文 件刚好满足这种需求,我们尝试通过它来实现一套 key-value 组件。
MMKV 原理

  • 内存准备
    通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操 作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
  • 数据组织
    数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
  • 写入优化
    考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
  • 空间增长
    使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可 控。我们需要在性能和空间上做个折中。

1.2 SharedPreference简介

SharedPreference 是安卓系统数据持久化的一种选择,用于存储轻量级 K-V 数据。SP 的读写方式为直接 IO 读取,即通过 FileInputStream/FileOutputStream 来进行读写,数据格式为 XML ,写入方式为全量更新。

SharedPreference

1.3 直接I/O读取与mmap

1.3.1 直接I/O

虚拟内存被操作系统划分成两块:用户空间和内核空间,用户空间是用户程序代码运行的地方,内核空 间是内核代码运行的地方。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。

直接I/O写文件流程
  1. 调用 write ,告诉内核需要写入数据的开始地址与长度
  2. 内核将数据拷贝到内核缓存
  3. 由操作系统调用,将数据拷贝到磁盘,完成写入

1.3.2 mmap

Linux 通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射 (memory mapping)
对文件进行 mmap,会在进程的虚拟内存分配地址空间,创建映射关系。实现这样的映射关系后,就可 以采用指针的方式读写操作这一段内存,而系统会自动回写到对应的文件。

mmap

1.3.3 mmap的优势

  • mmap 对文件的读写操作只需要从磁盘到用户主存的一次数据拷贝过程,减少了数据的拷贝次数, 提高了文件读写效率
  • mmap 使用逻辑内存对磁盘文件进行映射,操作内存就相当于操作文件,不需要开启线程,操作 mmap 的速度和操作内存的速度一样快
  • mmap 提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统如内存不足、进程退出等时候负责将内存回写到文件,不必担心 crash 导致数据丢失(见文首引文)
image-20201222150725142

1.4 MMKV数据结构

image-20201222150912350
image-20201222150926484

使用 010editor 打开 MMKV 映射文件

  • 前4个字节表示整个映射文件中数据的有效长度

    Q:为什么需要有效长度?

    A:因为 mmap 内存映射的时候,文件大小必须是 4096 (这个数字与操作系统位数有关)或者 其整数倍,所以并非整个文件内容都是有效数据,需要用有效数据长度来标明有效数据。

  • 从第五个字节开始,依次为k1长--->k1值--->v1长--->v1值--->k2长--->k2值--->v2长--->v2值---> ... --->

1.5 MMKV增量修改

我们介绍了 MMKV 数据在文件中的存储结构,这种结构如何实现增量修改的呢?

原因是 MMKV 在内存中是一个 map 表结构。完成首次 mmap 映射关系的建立后,之后我再次写入一个 同名 key 的 键值对,该键值对直接存储在映射文件的末尾,并修改有效长度。当我映射关系断开后重新 建立映射关系的时候,旧的键值对先写入 map 表,新的键值对后读入,将会覆盖掉旧的键值对,也就 实现了增量修改。

1.6 protobuf编码

1.6.1 protobuf编码规则

protobuf编码规则

我们以整型数据来说明
每一个字节的首位作标志位, 标志位为1 ,则表示该字节无法完整表示数据,需要更多的字节; 标志位 为0 ,则表示该字节已经是表示该数据的最后一个字节,该数据的读取到该字节为止。每一个字节的后 七位保存数据。

0x7f ?
ox7f 的10进制表示为 127 ,2 进制表示为 0111 1111 。
如果写入的数据 <= 0x7f ,那么一个字节的七个数据位足够表示这个数据,则字节首位置 0 ,后七位写入数据。
如果写入的数据 > 0x7f 那么一个字节的七个数据位不足以表示这个数据,则字节首位置1,后七位 写入数据,并将原数右移 7 位,继续执行判断。

1.6.2 protobuf编码举例

  • 编码 128 ,即 1000 0000

    1. 128 <= 127 ? (或者“1000 0000 用 7 位能否完整表示?”) NO!则字节首位置 1 ,后七位写入数 据 000 0000 ,得到第一个字节 1000 0000 ,将 128 右移 7 位,得到 0000 0001;

    2. 1 <= 127 ?(或者“0000 0001 用 7 位能否完整表示?”)YES!则字节首位置 0 ,后七位写入数据 000 0001 ,得到编码的最后一个字节 0000 0001 。

    3. 128 最后的 protobuf 编码为 1000 0000 | 0000 0001 ,从原本 int 的 4 字节,压缩到了现在的 2 字节

  • 读取 128 的 protobuf 编码,即 1000 0000 | 0000 0001

    1. 从前往后读取,读到第一个字节,即 1000 0000 ,首位为 1 ,说明该字节非表示该数据的最 后一个字节,则取出数据位 000 0000 暂存;

    2. 从前往后读取,读到第二个字节,即 0000 0001 ,首位为 0 ,说明该字节是表示该数据的最 后一个字节,则取出数据位 000 0001 暂存;

    3. 要明确,先存入的是低位,即先读出的也是低位。所以 后读出的字节要拼接到先读出字节的 前面构成高位,所以读取 1000 0000 0000 0001 结果是 000 0001 000 0000 ,即 1000 0000 或 128 。

你可能感兴趣的:(MMKV概览)