介绍
MMKV是基于mmap的键值存储库。提供了类似NSUserDefaults的功能。
MMKV的基础 - MMAP
mmap主要有2种用法,一个是建立匿名映射,可以起到父子进程之间共享内存的作用。另一个是磁盘文件映射进程的虚拟地址空间。MMKV就是用的磁盘文件映射。
mmap的主要的好处在于,减少一次内存拷贝。在我们平时的read/write系统调用中,文件内容的拷贝要多经历内核缓冲区这个阶段,所以比mmap多了一次内存拷贝,mmap只有用户空间的内存拷贝(这个阶段read/write也有)。正是因为减少了从Linux的页缓存到用户空间的缓冲区的这一次拷贝,所以mmap大大提高了性能,mmap也被称为zero-copy技术。
使用步骤
- 创建文件或者指定文件
- 打开文件
- 调整文件大小(非必须步骤)
- mmap内存映射
- 拷贝内容到映射区
- 扩容 (看需要)
- munmap结束映射
- 关闭文件
创建或者打开文件
没什么可说的,指定路径,创建文件。
打开文件
使用open函数,返回文件句柄
1 | _fd = open([url UTF8String], O_RDWR,S_IRWXU); |
获取文件大小
1 | size_t fileSize = 0; |
调整文件大小
如果设置的比文件小,则会截取文件。
1 | //代表将文件中多大的部分对应到内存。以字节为单位,不足一内存页按一内存页处理 |
文件内存映射
1 | void *start = NULL; //由系统选定地址 |
函数原型为void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
参数介绍:
start 传入一个期望的映射起始地址。同常传入null,由系统寻找合适的内存区域,并将地址返回。
length 传入映射的长度
port 映射区域的操作属性,有如下四种类型,这里我们使用读写属性。
1
2
3
4#define PROT_NONE 0x00 /* [MC2] no permissions */
#define PROT_READ 0x01 /* [MC2] pages can be read */
#define PROT_WRITE 0x02 /* [MC2] pages can be written */
#define PROT_EXEC 0x04 /* [MC2] pages can be executed */flag 会影响映射区域的各种特性,可以看下定义,类型比较多
fd 打开的文件句柄
offset 为文件映射的偏移量,通常设置为0,代表从文件最前方开始对应
扩容
需要三个步骤,使用ftruncate扩容文件,munmap结束映射,使用新的大小,重新映射。比如如下方法,是一个添加数据的方法,内存不够会扩容后继续添加
1 | - (void)appendData: (NSData *)data { |
MMKV 数据处理概要
一个MMKV实例会产生两个文件,内容文件,CRC校验文件。校验文件就是存储了内容文件中内容部分的CRC校验值。内容文件的前四个字节存储内容长度,后面这个长度的数据是实际内容。因为文件扩容是以页的整数倍。所以可能还会有一些空白内容在最后。
MMKV启动会读取文件,根据文件长度,把实际数据读取出来转成字典。其中还会进行CRC校验。
后续的操作,不管是读还是写,都是对这个字典进行操作。写操作之后会设置m_hasFullWriteBack = NO;
。表示有内容没有写回。之后会在合适的时机调用[self fullWriteBack]
进行数据写入。
MMKV set的实现
set的基本逻辑
字典中存的值都是NSData类型,所以数据在存入字典前,需要进行一下转化,如下是基本逻辑。
1 | - (BOOL)setFloat:(float)value forKey:(NSString *)key { |
MMKV 实现了MiniCodedOutputData
类,来处理字节信息。下面的类型到Data的转换就是通过此类完成。首先writeRawByte
方法,提供安字节写入的功能,直接填充一个字节,然后位置后移一位
1 | void MiniCodedOutputData::writeRawByte(uint8_t value) { |
不同的数据转成NSData的逻辑不一样,整体逻辑是分割成字节,然后使用writeRawByte
写入
Bool类型
Bool类型只占一个字节,直接写入即可
1 | void MiniCodedOutputData::writeBool(BOOL value) { |
Float类型
32位float类型按照小顶端顺序分成4个字节存入
1 | void MiniCodedOutputData::writeRawLittleEndian32(int32_t value) { |
64位的类似
1 | void MiniCodedOutputData::writeRawLittleEndian64(int64_t value) { |
Int类型
Int类型的存储逻辑稍有不同
1 | void MiniCodedOutputData::writeInt64(int64_t value) { |
64位Int类型是每次存7位,首位填1的方式存储。共需要10个字节。7*9 = 63,最后一个字节只存一位。这里每次只处理7位的原因,会在后续进行说明。
32位的Int类型类似
1 | void MiniCodedOutputData::writeRawVarint32(int32_t value) { |
NSData类型
data类型的写入,需要记录data的长度,以及值
1 | void MiniCodedOutputData::writeData(NSData *value) { |
长度的写入已经介绍过了,下面看下Data类型的写入。
1 | void MiniCodedOutputData::writeRawData(NSData *data) { |
data类型比较容易处理,直接内存拷贝即可。
NSString类型
1 | void MiniCodedOutputData::writeString(NSString *value) { |
NSString 类型和Data类似,先写入长度,在转成Data存入。NSString有个方便的方法,直接将数据放到指定的内存位置。这里正好合用。
取数据的基本逻辑
取数据的逻辑是对set的反向操作,先从字典中取出数据,然后转成具体类型。
1 | - (int64_t)getInt64ForKey:(NSString *)key { |
MMKV实现了MiniCodedInputData来将NSData转成需要的类型。这个类的操作是MiniCodedOutputData的反向操作。其他都比较类似,这里看一下readRawVarint64
方法,解释下之前的留下的小问题,为什么要每7位存一个字节。
1 | int64_t MiniCodedInputData::readRawVarint64() { |
读取的字节是存到一个8位Int中 int8_t b = this->readRawByte();
。前面的逻辑中每次存入七位,首位填1,因为首位是填充的,没有意义,这里通过 & 0x7f直接移除。如果是8位存储,首位的1会使这个数据变成负数,这里的处理就会变得比较麻烦。