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 为内存映射的映射区的内存起始地址。若还没进行内存映射,或映射失败,则执行以下步骤,开始内存映射:
判断文件是否存在,若不存在,则创建文件 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()必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射
若设置文件 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(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 值的步骤:
- 检查存储的文件是否已经加载进内存 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];
}
参考文献
- 内存映射文件原理探索
- 获取文件信息 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结构
- crc32 校验
- crc校验原理
- Protocol Buffer 序列化原理大揭秘
- 图解Protobuf编码