MMKV 源码详解
MMKV 简介
MMKV——基于 mmap 的高性能通用 key-value 组件
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。目前已移植到 Android / macOS / Win32 / POSIX 平台,一并开源。
MMKV 实现方案
• 内存准备
通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
• 数据组织
数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
• 写入优化
考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
• 空间增长
使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
• 数据有效性
考虑到文件系统、操作系统都有一定的不稳定性,另外增加了 crc 校验,对无效数据进行甄别。在 iOS 微信现网环境上,有平均约 70 万/日 次的数据校验不通过。
MMAP 原理简介
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图所示:
MMAP 原理.png
由上图可以看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。
mmap 和常规文件操作的区别
常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。写操作也是一样,待写入的 buffer 在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存储器,再写回磁盘中(延迟写回),也是需要两次数据拷贝。
而使用 mmap 操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。
mmap 使用细节
使用 mmap 需要注意的一个关键点是,mmap 映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是 4k 字节)。原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap 从磁盘到虚拟地址空间的映射也必须是页。
CRC校验原理简介
CRC(Cyclic Redundancy Check)原理:在 K 位信息码(目标发送数据)后再拼接 R 位校验码,使整个编码长度为 N 位,因此这种编码也叫(N,K)码。通俗的说,就是在需要发送的信息后面附加一个数(即校验码),生成一个新的发送数据发送给接收端。这个数据要求能够使生成的新数据被一个特定的数使用模2除法(即异或运算)整除。
CRC校验原理.png
ProtoBuf 编码原理简介
ProtoBuf 是 Google 出品的一种结构数据序列化方法,可简单类比于 XML,其具有以下特点:
- 语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
- 高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
- 扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序
-
采用 独特编码方式 & T - L - V 的数据存储方式。即 Tag - Length - Value,标识 - 长度 - 字段值 存储方式
ProtoBuf 编码原理.png
Varints 编码的规则
- 在每个字节开头的 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节
- 存储数字对应的二进制补码
- 补码的低位排在前面
int32 val = 666; // 设置一个 int32 的字段的值 val = 666; 这时编码的结果如下
原码:000 ... 101 0011010 // 666 的源码
补码:000 ... 101 0011010 // 666 的补码
Varints 编码:1#0011010 0#000 0101 (9a 05) // 666 的 Varints 编码
编码数字 666,Varints 只使用了 2 个字节。而正常情况下 int32 需要使用 4 个字节
Wire_Type
String 类型编码
message Test {
required string str = 2;
}
Test.setStr(“testing”)
// 经过protobuf编码序列化后的数据以二进制的方式输出
// 输出为:18, 7, 116, 101, 115, 116, 105, 110, 103
String 类型编码.png
Protobuf 协议在 MMKV 中 MiniCodedOutputData 实现的差异
- Protobuf 采用 独特编码方式 & Tag - Length - Value 的数据存储方式。MiniCodedOutputData 采用的是 Length - Value 的数据存储方式。MiniCodedOutputData 不需要 Tag (field_number+wire_type) 是因为: key-value 不需要唯一标识,用户读或写 key-value 时,是知道 value 的类型的,因此不需要通过 tag 读取。
- Protobuf 推荐对负数使用 sint32 或 sint64 编码,可以减少字节使用。MiniCodedOutputData 对负数统一使用 Varints 编码,分配 10 字节。
源码概览(MMKV version:1.0.23)
[MMKV defaultMMKV]
ViewController.mm
- (void)viewDidLoad {
[super viewDidLoad];
// 设置日志级别,只有当输出日志的级别高于或等于此日志级别时,才会输出日志
[MMKV setLogLevel:MMKVLogInfo];
// 日志级别为 MMKVLogNone 时,不输出日志,此级别是最高日志级别
//[MMKV setLogLevel:MMKVLogNone];
// register handler
[MMKV registerHandler:self];
// not necessary: set MMKV's root dir
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libraryPath = (NSString *) [paths firstObject];
if ([libraryPath length] > 0) {
NSString *rootDir = [libraryPath stringByAppendingPathComponent:@"mmkv"];
[MMKV setMMKVBasePath:rootDir];
}
[self funcionalTest];
[self testReKey];
//[self testImportFromUserDefault];
//[self testCornerSize];
//[self testFastRemoveCornerSize];
DemoSwiftUsage *swiftUsageDemo = [[DemoSwiftUsage alloc] init];
[swiftUsageDemo testSwiftFunctionality];
m_loops = 10000;
m_arrStrings = [NSMutableArray arrayWithCapacity:m_loops];
m_arrStrKeys = [NSMutableArray arrayWithCapacity:m_loops];
m_arrIntKeys = [NSMutableArray arrayWithCapacity:m_loops];
for (size_t index = 0; index < m_loops; index++) {
NSString *str = [NSString stringWithFormat:@"%s-%d", __FILE__, rand()];
[m_arrStrings addObject:str];
NSString *strKey = [NSString stringWithFormat:@"str-%zu", index];
[m_arrStrKeys addObject:strKey];
NSString *intKey = [NSString stringWithFormat:@"int-%zu", index];
[m_arrIntKeys addObject:intKey];
}
}
[MMKV setLogLevel:MMKVLogInfo] 详解
MMKV.mm
+ (void)setLogLevel:(MMKVLogLevel)logLevel {
CScopedLock lock(g_instanceLock);
g_currentLogLevel = logLevel;
}
此方法用来设置日志级别,只有当输出日志的级别高于或等于此日志级别时,才会输出日志,代码实现在文件 MMKVLog.mm 中,如下所示:
void _MMKVLogWithLevel(MMKVLogLevel level, const char *file, const char *func, int line, NSString *format, ...) {
if (level >= g_currentLogLevel) {
// 输出日志
}
}
。 CScopedLock 类的实现在 ScopedLock.hpp 文件中。如下代码所示:
class CScopedLock {
NSRecursiveLock *m_oLock;
public:
CScopedLock(NSRecursiveLock *oLock) : m_oLock(oLock) { [m_oLock lock]; }
~CScopedLock() {
[m_oLock unlock];
m_oLock = nil;
}
};
lock(g_instanceLock) 是构造方法,在此方法里初始化了一个递归锁,并调用递归锁的 lock 方法,起到安全的输出日志的功能。构造方法中的传参 g_instanceLock 是一个全局的静态变量,在 MMKV.mm 文件的 + (void)initialize 方法中赋值为一个递归锁实例对象。
+ (void)initialize {
if (self == MMKV.class) {
...
g_instanceLock = [[NSRecursiveLock alloc] init];
...
}
}
[MMKV registerHandler:self] 详解
MMKV.mm
+ (void)registerHandler:(id<MMKVHandler>)handler {
CScopedLock lock(g_instanceLock);
g_callbackHandler = handler;
if ([g_callbackHandler respondsToSelector:@selector(mmkvLogWithLevel:file:line:func:message:)]) {
g_isLogRedirecting = true;
...
}
}
此方法注册 self(即:ViewController) 为 MMKVHandler 协议的实现类,该协议的定义在文件 MMKVHandler.h 中,如下所示:
@protocol MMKVHandler <NSObject>
@optional
// by default MMKV will discard all datas on crc32-check failure
// return `MMKVOnErrorRecover` to recover any data on the file
- (MMKVRecoverStrategic)onMMKVCRCCheckFail:(NSString *)mmapID;
// by default MMKV will discard all datas on file length mismatch
// return `MMKVOnErrorRecover` to recover any data on the file
- (MMKVRecoverStrategic)onMMKVFileLengthError:(NSString *)mmapID;
// by default MMKV will print log using NSLog
// implement this method to redirect MMKV's log
- (void)mmkvLogWithLevel:(MMKVLogLevel)level file:(const char *)file line:(int)line func:(const char *)funcname message:(NSString *)message;
@end
[MMKV setMMKVBasePath:rootDir] 详解
MMKV.mm
+ (void)setMMKVBasePath:(NSString *)basePath {
if (basePath.length > 0) {
g_basePath = basePath;
...
}
}
此方法用于设置 key-value 存储的根路径,可以不必设置,默认的存储根路径在 Document/mmkv 目录下,代码如下:
static NSString *g_basePath = nil;
+ (NSString *)mmkvBasePath {
if (g_basePath.length > 0) {
return g_basePath;
}
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = (NSString *) [paths firstObject];
if ([documentPath length] > 0) {
g_basePath = [documentPath stringByAppendingPathComponent:@"mmkv"];
return g_basePath;
} else {
return @"";
}
}
[self funcionalTest] 详解
ViewController.mm
- (void)funcionalTest {
// MMKV *mmkv = [MMKV defaultMMKV];
auto path = [MMKV mmkvBasePath];
path = [path stringByDeletingLastPathComponent];
path = [path stringByAppendingPathComponent:@"mmkv_2"];
auto mmkv = [MMKV mmkvWithID:@"test/case1" relativePath:path];
[mmkv setBool:YES forKey:@"bool"];
NSLog(@"bool:%d", [mmkv getBoolForKey:@"bool"]);
...
[mmkv removeValueForKey:@"bool"];
NSLog(@"bool:%d", [mmkv getBoolForKey:@"bool"]);
[mmkv close];
}
此方法是获取一个指定目录下、特定 ID 的 MMKV 对象 mmkv,用该对象存储、移除 key-value,及关闭 mmkv 对象。
close 关闭 mmkv 对象
/ call this method if the instance is no longer needed in the near future
// any subsequent call to the instance is undefined behavior
- (void)close;
auto mmkv = [MMKV mmkvWithID:@"test/case1" relativePath:path] 详解
// mmapID: any unique ID (com.tencent.xin.pay, etc)
// if you want a per-user mmkv, you could merge user-id within mmapID
// relativePath: custom path of the file, `NSDocumentDirectory/mmkv` by default
+ (instancetype)mmkvWithID:(NSString *)mmapID relativePath:(nullable NSString *)path {
return [self mmkvWithID:mmapID cryptKey:nil relativePath:path];
}
+ (instancetype)mmkvWithID:(NSString *)mmapID cryptKey:(NSData *)cryptKey relativePath:(nullable NSString *)relativePath {
...
NSString *kvPath = [MMKV mappedKVPathWithID:mmapID relativePath:relativePath];
...
NSString *kvKey = [MMKV mmapKeyWithMMapID:mmapID relativePath:relativePath];
CScopedLock lock(g_instanceLock);
MMKV *kv = [g_instanceDic objectForKey:kvKey];
if (kv == nil) {
kv = [[MMKV alloc] initWithMMapID:kvKey cryptKey:cryptKey path:kvPath];
[g_instanceDic setObject:kv forKey:kvKey];
}
return kv;
}
此方法返回一个指定目录下、特定 ID 的 MMKV 对象 mmkv。首先 [MMKV mappedKVPathWithID:mmapID relativePath:relativePath] 获取 key-value 存储路径,然后 [MMKV mmapKeyWithMMapID:mmapID relativePath:relativePath] 构造 kvKey,并从静态字典 static NSMutableDictionary *g_instanceDic 中,获取一个 MMKV 对象 kv: [g_instanceDic objectForKey:kvKey] ,若字典中不存在该对象,则创建该对象,并设置到 g_instanceDic 字典中。
创建该对象的方法如下:
- (instancetype)initWithMMapID:(NSString *)kvKey cryptKey:(NSData *)cryptKey path:(NSString *)path {
if (self = [super init]) {
m_lock = [[NSRecursiveLock alloc] init];
m_mmapID = kvKey;
m_path = path;
m_crcPath = [MMKV crcPathWithMappedKVPath:m_path];
if (cryptKey.length > 0) {
m_cryptor = new AESCrypt((const unsigned char *) cryptKey.bytes, cryptKey.length);
}
[self loadFromFile];
...
}
return self;
}
- (void) loadFromFile {
[self prepareMetaFile];
...
}
- (void)prepareMetaFile {
if (m_metaFilePtr == nullptr || m_metaFilePtr == MAP_FAILED) {
if (!isFileExist(m_crcPath)) {
createFile(m_crcPath);
}
m_metaFd = open(m_crcPath.UTF8String, O_RDWR, S_IRWXU);
if (m_metaFd < 0) {
MMKVError(@"fail to open:%@, %s", m_crcPath, strerror(errno));
removeFile(m_crcPath);
} else {
size_t size = 0;
struct stat st = {};
if (fstat(m_metaFd, &st) != -1) {
size = (size_t) st.st_size;
}
int fileLegth = CRC_FILE_SIZE;
if (size != fileLegth) {
size = fileLegth;
if (ftruncate(m_metaFd, size) != 0) {
MMKVError(@"fail to truncate [%@] to size %zu, %s", m_crcPath, size, strerror(errno));
close(m_metaFd);
m_metaFd = -1;
removeFile(m_crcPath);
return;
}
}
m_metaFilePtr = (char *) mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, m_metaFd, 0);
if (m_metaFilePtr == MAP_FAILED) {
MMKVError(@"fail to mmap [%@], %s", m_crcPath, strerror(errno));
close(m_metaFd);
m_metaFd = -1;
}
}
}
}
m_crcPath = [MMKV crcPathWithMappedKVPath:m_path] m_crcPath 是在 m_path 后拼接上 ".crc" 字符串。
loadFromFile 方法首先调用 prepareMetaFile 方法。 prepareMetaFile m_metaFilePtr 为内存映射的映射区的内存起始地址。若还没进行内存映射,或映射失败,则执行以下步骤,开始内存映射:
-
判断文件是否存在,若不存在,则创建文件 createFile(m_crcPath)
-
以可读、可写的权限打开文件,得到文件描述符 m_metaFd = open(m_crcPath.UTF8String, O_RDWR, S_IRWXU)
-
如果 m_metaFd < 0,即文件打开失败,则取消对该文件的链接 removeFile(m_crcPath) ,否则,获取文件的属性 fstat(m_metaFd, &st)
-
判断文件的 size 是否等于 CRC_FILE_SIZE (即:getpagesize(), 系统的分页大小),若不相等,则 size = fileLegth,并将文件大小改变为参数 size 指定的大小 ftruncate(m_metaFd, size)。为何要将 size 设为 getpagesize() ?原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。
-
若设置文件 size 失败,则关闭文件 close(m_metaFd) ,并取消链接文件 removeFile(m_crcPath)。 否则,将文件映射到进程地址空间 m_metaFilePtr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, m_metaFd, 0) ,映射区域可被读取、写入、对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享
-
如果映射失败,则关闭文件
loadFromFile
介绍完 loadFromFile 方法中的 prepareMetaFile 方法,我们继续往下看代码:
- (void) loadFromFile {
[self prepareMetaFile];
if (m_metaFilePtr != nullptr && m_metaFilePtr != MAP_FAILED) {
m_metaInfo.read(m_metaFilePtr);
}
if (m_cryptor) {
if (m_metaInfo.m_version >= 2) {
m_cryptor->reset(m_metaInfo.m_vector, sizeof(m_metaInfo.m_vector));
}
}
m_fd = open(m_path.UTF8String, O_RDWR, S_IRWXU);
if (m_fd < 0) {
MMKVError(@"fail to open:%@, %s", m_path, strerror(errno));
} else {
m_size = 0;
struct stat st = {};
if (fstat(m_fd, &st) != -1) {
m_size = (size_t) st.st_size;
}
// round up to (n * pagesize)
if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
if (ftruncate(m_fd, m_size) != 0) {
MMKVError(@"fail to truncate [%@] to size %zu, %s", m_mmapID, m_size, strerror(errno));
m_size = (size_t) st.st_size;
return;
}
}
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
MMKVError(@"fail to mmap [%@], %s", m_mmapID, strerror(errno));
} else {
const int offset = pbFixed32Size(0);
NSData *lenBuffer = [NSData dataWithBytesNoCopy:m_ptr length:offset freeWhenDone:NO];
@try {
m_actualSize = MiniCodedInputData(lenBuffer).readFixed32();
} @catch (NSException *exception) {
MMKVError(@"%@", exception);
}
MMKVInfo(@"loading [%@] with %zu size in total, file size is %zu", m_mmapID, m_actualSize, m_size);
if (m_actualSize > 0) {
bool loadFromFile, needFullWriteback = false;
if (m_actualSize < m_size && m_actualSize + offset <= m_size) {
if ([self checkFileCRCValid] == YES) {
loadFromFile = true;
} else {
loadFromFile = false;
if (g_callbackHandler && [g_callbackHandler respondsToSelector:@selector(onMMKVCRCCheckFail:)]) {
auto strategic = [g_callbackHandler onMMKVCRCCheckFail:m_mmapID];
if (strategic == MMKVOnErrorRecover) {
loadFromFile = true;
needFullWriteback = true;
}
}
}
} else {
MMKVError(@"load [%@] error: %zu size in total, file size is %zu", m_mmapID, m_actualSize, m_size);
loadFromFile = false;
if (g_callbackHandler && [g_callbackHandler respondsToSelector:@selector(onMMKVFileLengthError:)]) {
auto strategic = [g_callbackHandler onMMKVFileLengthError:m_mmapID];
if (strategic == MMKVOnErrorRecover) {
loadFromFile = true;
needFullWriteback = true;
[self writeActualSize:m_size - offset];
}
}
}
if (loadFromFile) {
MMKVInfo(@"loading [%@] with crc %u sequence %u", m_mmapID, m_metaInfo.m_crcDigest, m_metaInfo.m_sequence);
NSData *inputBuffer = [NSData dataWithBytesNoCopy:m_ptr + offset length:m_actualSize freeWhenDone:NO];
if (m_cryptor) {
inputBuffer = decryptBuffer(*m_cryptor, inputBuffer);
}
m_dic = [MiniPBCoder decodeContainerOfClass:NSMutableDictionary.class withValueClass:NSData.class fromData:inputBuffer];
m_output = new MiniCodedOutputData(m_ptr + offset + m_actualSize, m_size - offset - m_actualSize);
if (needFullWriteback) {
[self fullWriteBack];
}
} else {
[self writeActualSize:0];
m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
[self recaculateCRCDigest];
}
} else {
m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
[self recaculateCRCDigest];
}
MMKVInfo(@"loaded [%@] with %zu values", m_mmapID, (unsigned long) m_dic.count);
}
}
if (m_dic == nil) {
m_dic = [NSMutableDictionary dictionary];
}
if (![self isFileValid]) {
MMKVWarning(@"[%@] file not valid", m_mmapID);
}
tryResetFileProtection(m_path);
tryResetFileProtection(m_crcPath);
m_needLoadFromFile = NO;
}
上面的代码归纳为以下几个步骤:
- 读取元文件的信息 m_metaInfo.read(m_metaFilePtr) ,内部使用 void * memcpy ( void * dest, const void * src, size_t num ) 函数,该函数的作用为:复制 src 所指的内存内容的前 num 个字节到 dest 所指的内存地址上。如果 MMKV 对象初始化的时,传了 AES 加密的 key : cryptKey ,且当前的 m_metaInfo.m_version >= 2 时,则将元文件的 AES 加密重置 m_cryptor->reset(m_metaInfo.m_vector, sizeof(m_metaInfo.m_vector))。
- 打开 m_path 路径下的文件 m_fd ,给 m_fd 文件分配适合的 m_size ,然后进行内存映射。
- 从文件中读取前 4 个字节 pbFixed32Size(0),将前四个字节翻转后得到的字节 readFixed32() ,得到存储的数据实际占用的空间 。
- 若数据为空 m_actualSize == 0,则重置 m_output m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset) ,并重新计算 crc32 校验码 [self recaculateCRCDigest] 。
- 若数据不为空 m_actualSize > 0 :
- 在 m_actualSize + offset <= m_size 时,如果通过了 crc32 校验,则可以加载文件。否则, 如果代理对文件校验失败的处理策略为 MMKVOnErrorRecover ,则也可以加载文件,并且对文件重写。
- 在 m_actualSize > m_size || m_actualSize + offset > m_size 时,如果文件长度错误,可以加载文件,并且对文件重写。
- 加载文件的过程 loadFromFile : 读取-->解密-->解码-->重置输出数据对象,如果需要对文件重写,则执行重写过程。
NSData *inputBuffer = [NSData dataWithBytesNoCopy:m_ptr + offset length:m_actualSize freeWhenDone:NO];
if (m_cryptor) {
inputBuffer = decryptBuffer(*m_cryptor, inputBuffer);
}
m_dic = [MiniPBCoder decodeContainerOfClass:NSMutableDictionary.class withValueClass:NSData.class fromData:inputBuffer];
m_output = new MiniCodedOutputData(m_ptr + offset + m_actualSize, m_size - offset - m_actualSize);
if (needFullWriteback) {
[self fullWriteBack];
}
- 不加载文件的话,则重置 m_output m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset) ,并重新计算 crc32 校验码 [self recaculateCRCDigest] 。
- 对 m_path 和 m_crcPath 文件尝试重置文件保护。
[mmkv setBool:YES forKey:@"bool"]
- (BOOL)setBool:(BOOL)value forKey:(NSString *)key {
if (key.length <= 0) {
return NO;
}
size_t size = pbBoolSize(value);
NSMutableData *data = [NSMutableData dataWithLength:size];
MiniCodedOutputData output(data);
output.writeBool(value);
return [self setRawData:data forKey:key];
}
[self setRawData:data forKey:key]
BOOL 值占 1 字节,写入二进制文件。将 key 和 data 写入内存映射文件 。若写入成功,则更新内存里的 key-value 字典 m_dic 。
- (BOOL)setRawData:(NSData *)data forKey:(NSString *)key {
if (data.length <= 0 || key.length <= 0) {
return NO;
}
CScopedLock lock(m_lock);
auto ret = [self appendData:data forKey:key];
if (ret) {
[m_dic setObject:data forKey:key];
m_hasFullWriteBack = NO;
}
return ret;
}
- (BOOL)appendData:(NSData *)data forKey:(NSString *)key
将 key 和 data 写入内存映射文件 的步骤:
- 计算 key 的 size, 以及为存储 key 的 size 所占的 size;计算 value 的 size, 以及为存储 value 的 size 所占的 size;
- 确保内存映射的空间足以存储数据,若不足,则分配一个更大的空间,重新进行内存映射
[self ensureMemorySize:size]
。 - 将 key 和 data 写入内存映射文件。
- 写入成功后,若需要加密,将刚写入的数据进行加密。
- 更新 CRC32 验证码。
- (BOOL)appendData:(NSData *)data forKey:(NSString *)key {
size_t keyLength = [key lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
auto size = keyLength + pbRawVarint32Size((int32_t) keyLength); // size needed to encode the key
size += data.length + pbRawVarint32Size((int32_t) data.length); // size needed to encode the value
BOOL hasEnoughSize = [self ensureMemorySize:size];
if (hasEnoughSize == NO || [self isFileValid] == NO) {
return NO;
}
BOOL ret = [self writeActualSize:m_actualSize + size];
if (ret) {
ret = [self protectFromBackgroundWriting:size
writeBlock:^(MiniCodedOutputData *output) {
output->writeString(key);
output->writeData(data); // note: write size of data
}];
if (ret) {
static const int offset = pbFixed32Size(0);
auto ptr = (uint8_t *) m_ptr + offset + m_actualSize - size;
if (m_cryptor) {
m_cryptor->encrypt(ptr, ptr, size);
}
[self updateCRCDigest:ptr withSize:size increaseSequence:KeepSequence];
}
}
return ret;
}
[self ensureMemorySize:size]
官方说明 :使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。我们需要在性能和空间上做个折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。
- (BOOL)ensureMemorySize:(size_t)newSize {
...
// make some room for placeholder
constexpr uint32_t /*ItemSizeHolder = 0x00ffffff,*/ ItemSizeHolderSize = 4;
if (m_dic.count == 0) {
newSize += ItemSizeHolderSize;
}
if (newSize >= m_output->spaceLeft() || m_dic.count == 0) {
// 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);
...
m_ptr = (char *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
...
}
...
delete m_output;
m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
BOOL ret = [self protectFromBackgroundWriting:m_actualSize
writeBlock:^(MiniCodedOutputData *output) {
output->writeRawData(data);
}];
if (ret) {
[self recaculateCRCDigest];
}
return ret;
}
return YES;
}
[mmkv getBoolForKey:@"bool"]
用 key 获取 bool 值的步骤:
- 检查存储的文件是否已经加载进内存 checkLoadData ,若未加载,则调用 loadFromFile 加载,加载成功后会存入字典 m_dict ;
- 从 m_dict 中获取编码过的二进制数据;
- 解码 input.readBool()。
- (BOOL)getBoolForKey:(NSString *)key {
return [self getBoolForKey:key defaultValue:FALSE];
}
- (BOOL)getBoolForKey:(NSString *)key defaultValue:(BOOL)defaultValue {
if (key.length <= 0) {
return defaultValue;
}
NSData *data = [self getRawDataForKey:key];
if (data.length > 0) {
@try {
MiniCodedInputData input(data);
return input.readBool();
} @catch (NSException *exception) {
MMKVError(@"%@", exception);
}
}
return defaultValue;
}
- (NSData *)getRawDataForKey:(NSString *)key {
CScopedLock lock(m_lock);
[self checkLoadData];
return [m_dic objectForKey:key];
}
- (void)checkLoadData {
// CScopedLock lock(m_lock);
if (m_needLoadFromFile == NO) {
return;
}
m_needLoadFromFile = NO;
[self loadFromFile];
}
[mmkv removeValueForKey:@"bool"]
移除 key-value 的过程:
- m_dic 字典调用 removeObjectForKey:key
- 将 key 对应的 value 写入空二进制数据 ,如此在 getBoolForKey: 时,由于空二进制数据的 length 不大于零,从而返回默认值
- (BOOL)getBoolForKey:(NSString *)key defaultValue:(BOOL)defaultValue {
...
NSData *data = [self getRawDataForKey:key];
if (data.length > 0) { ... }
return defaultValue;
}
- (void)removeValueForKey:(NSString *)key {
if (key.length <= 0) {
return;
}
CScopedLock lock(m_lock);
[self checkLoadData];
if ([m_dic objectForKey:key] == nil) {
return;
}
[m_dic removeObjectForKey:key];
m_hasFullWriteBack = NO;
static NSData *data = [NSData data];
[self appendData:data forKey:key];
}
[mmkv setObject:(nullable NSObject<NSCoding> *)object forKey:(NSString *)key]
[MiniPBCoder encodeDataWithObject:object]
getObjectOfClass:(Class)cls forKey:(NSString *)key
参考文献
- 认真分析mmap:是什么 为什么 怎么用
- 获取文件信息 stat函数(stat、fstat、lstat)
- 自动判断变量类型,类似于 swift 中的 let 浅析C语言auto关键字和C++ 中的auto关键字
- 根据不同的开发环境编译不同的代码 关于__IPHONE_OS_VERSION_MAX_ALLOWED和__IPHONE_OS_VERSION_MIN_REQUIRED
- 创建指定属性的文件 iOS小记--NSFileProtectionKey
- Linux C ftruncate 函数修改文件大小
- 共享内存映射之mmap()函数详解
- constexpr关键字的作用
- C语言 mmap()函数(建立内存映射) 与 munmap()函数(解除内存映射)
- C memcpy()用法
- C++ 类构造函数 & 析构函数
- C++中的.hpp文件
- C++ open 打开文件
- 文件处理常用方法及link和unlink讲解
- linux C语言 getpagesize() 获得页内存大小
- C++基础学习】C++中union结构
- crc校验原理
- 深入 ProtoBuf - 编码
- ProtoBuf 官网
- 常见的序列化框架及Protobuf序列化原理
- ProtoBuf的使用以及原理分析