MMKV 源码详解

MMKV 简介

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

MMKV 原理

MMKV 原理

源码概览

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)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 
@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 {
    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 为内存映射的映射区的内存起始地址。若还没进行内存映射,或映射失败,则执行以下步骤,开始内存映射:

  1. 判断文件是否存在,若不存在,则创建文件 createFile(m_crcPath)

  2. 以可读、可写的权限打开文件,得到文件描述符 m_metaFd = open(m_crcPath.UTF8String, O_RDWR, S_IRWXU)

  3. 如果 m_metaFd < 0,即文件打开失败,则取消对该文件的链接 removeFile(m_crcPath) ,否则,获取文件的属性 fstat(m_metaFd, &st)

  4. 判断文件的 size 是否等于 CRC_FILE_SIZE (即:getpagesize(), 系统的分页大小),若不相等,则 size = fileLegth,并将文件大小改变为参数 size 指定的大小 ftruncate(m_metaFd, size)。为何要将 size 设为 getpagesize() ?原因:mmap()必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射

  5. 若设置文件 size 失败,则关闭文件 close(m_metaFd) ,并取消链接文件 removeFile(m_crcPath)。 否则,将文件映射到进程地址空间 m_metaFilePtr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, m_metaFd, 0) ,映射区域可被读取、写入、对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享

  6. 如果映射失败,则关闭文件

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;
}

上面的代码归纳为以下几个步骤:

  1. 读取元文件的信息 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))
  2. 打开 m_path 路径下的文件 m_fd ,给 m_fd 文件分配适合的 m_size ,然后进行内存映射。
  3. 从文件中读取前 4 个字节 pbFixed32Size(0),将前四个字节翻转后得到的字节 readFixed32() ,得到存储的数据实际占用的空间 。
  4. 若数据为空 m_actualSize == 0,则重置 m_output m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset) ,并重新计算 crc32 校验码 [self recaculateCRCDigest]
  5. 若数据不为空 m_actualSize > 0 :
    • m_actualSize + offset <= m_size 时,如果通过了 crc32 校验,则可以加载文件。否则, 如果代理对文件校验失败的处理策略为 MMKVOnErrorRecover ,则也可以加载文件,并且对文件重写。
    • m_actualSize > m_size || m_actualSize + offset > m_size 时,如果文件长度错误,可以加载文件,并且对文件重写。
  6. 加载文件的过程 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];
}
  1. 不加载文件的话,则重置 m_output m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset) ,并重新计算 crc32 校验码 [self recaculateCRCDigest]
  2. m_pathm_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 字节,写入二进制文件。将 keydata 写入内存映射文件 。若写入成功,则更新内存里的 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
keydata 写入内存映射文件 的步骤:

  1. 计算 key 的 size, 以及为存储 key 的 size 所占的 size;计算 value 的 size, 以及为存储 value 的 size 所占的 size;
  2. 确保内存映射的空间足以存储数据,若不足,则分配一个更大的空间,重新进行内存映射 [self ensureMemorySize:size]
  3. keydata 写入内存映射文件。
  4. 写入成功后,若需要加密,将刚写入的数据进行加密。
  5. 更新 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(1, m_dic.count);
        size_t futureUsage = avgItemSize * std::max(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 值的步骤:

  1. 检查存储的文件是否已经加载进内存 checkLoadData ,若未加载,则调用 loadFromFile 加载,加载成功后会存入字典 m_dict
  2. m_dict 中获取编码过的二进制数据;
  3. 解码 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 的过程:

  1. m_dic 字典调用 removeObjectForKey:key
  2. 将 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];
}

参考文献

  1. 内存映射文件原理探索
  2. 获取文件信息 stat函数(stat、fstat、lstat)
  3. 自动判断变量类型,类似于 swift 中的 let 浅析C语言auto关键字和C++ 中的auto关键字
  4. 根据不同的开发环境编译不同的代码 关于__IPHONE_OS_VERSION_MAX_ALLOWED和__IPHONE_OS_VERSION_MIN_REQUIRED
  5. 创建指定属性的文件 iOS小记--NSFileProtectionKey
  6. Linux C ftruncate 函数修改文件大小
  7. 共享内存映射之mmap()函数详解
  8. constexpr关键字的作用
  9. C语言 mmap()函数(建立内存映射) 与 munmap()函数(解除内存映射)
  10. C memcpy()用法
  11. C++ 类构造函数 & 析构函数
  12. C++中的.hpp文件
  13. C++ open 打开文件
  14. 文件处理常用方法及link和unlink讲解
  15. linux C语言 getpagesize() 获得页内存大小
  16. C++基础学习】C++中union结构
  17. crc32 校验
  18. crc校验原理
  19. Protocol Buffer 序列化原理大揭秘
  20. 图解Protobuf编码

你可能感兴趣的:(MMKV 源码详解)