前言
MMKV是微信开源的数据持久化框架,现在已经支持Android/iOS/PC 平台。该框架是基于mmap映射内存的key—value组件,使用protobuf实现数据的序列化和反序列化,性能高,稳定性强。微信在2015就在微信应用上使用了该框架。实验证明MMKV是数据持久化的首选。
mmap内存映射是什么?
我们知道数据的的读取与写入都是操作沙盒内的文件,每个应用程序都有限定的沙盒。读取写入数据的操作步骤:获取沙盒文件夹路径 ->创建文件路径 -> 使用文件管理对象创建文件 -> 创建文件对接对象 ->读取或写入数据 -> 关闭文件。需要频繁写入读取时,这样就非常消耗资源,mmap就是提高写入读取效率的,mmap映射内存是将磁盘里的文件映射到进程的虚拟内存中,根据映射文件指针读取数据时,系统会返回内核中的数据,通过 mmap 内存映射磁盘上的文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由 iOS 负责将内存回写到文件,不必担心 crash 导致数据丢失,大大降低了程序异常带来的数据丢失率。 mmap内存映射protobuf是什么?
ProtoBuf是由google公司用于数据交换的序列结构化数据格式,具有跨平台、跨语言、可扩展特性,类型于常用的XML及JSON,但具有更小的传输体积、更高的编码、解码能力,特别适合于数据存储、网络数据传输等对存储体积、实时性要求高的领域。
优点:空间效率搞,时间效率要高,对于数据大小敏感,传输效率高的。
缺点:消息结构可读性不高,目前使用不广泛。
MMKV 源码分析
MMKV设计MMKV维护了一个<String,AnyObject>的dic,在写入数据时,会在dit和mmap映射区写入相同的数据,最后由内核同步到文件。因为dic和文件数据同步,所以读取时直接去dit中的值。MMKV数据持久化的步骤:mmap 内存映射 -> 写数据 -> 读数据 -> crc校验 -> aes加密。
在MMKV的源码中,是怎么样内存映射的呢?
- (void)loadFromFile {
// open 得到文件描述符m_fd
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;
}
}
// 1: 映射内存,用来将某个文件内容映射到内存中,对该内存区域的存取即是直接对该文件内容的读写
//参数1 :nullptr 对应内存的起始地址
//参数2 :m_size 按页对齐后的文件大小
//参数3:m_fd 映射到内存的文件
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) { // 当文件中有记录时,如果第一次使用或是已经清理过,实际使用空间将为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 writeAcutalSize:m_size - offset]; // 重新记录下文件的相关信息
}
}
}
if (loadFromFile) { // 假定文件是正常的,从文件中读取
NSData *inputBuffer = [NSData dataWithBytesNoCopy:m_ptr + offset length:m_actualSize freeWhenDone:NO];
if (m_cryptor) {
//对文件数据进行AES加密(对称加密算法,加密与解密使用相同的秘钥)
inputBuffer = decryptBuffer(*m_cryptor, inputBuffer);
}
// 2. 初始化m_dic
// 如果文件存在错误(例如crc校验不通过),会导致数据错误或是丢失
m_dic = [MiniPBCoder decodeContainerOfClass:NSMutableDictionary.class withValueClass:NSData.class fromData:inputBuffer];
// 使用MiniCodedOutputData将数据按字节拷贝到指定区域
m_output = new MiniCodedOutputData(m_ptr + offset + m_actualSize, m_size - offset - m_actualSize);
// 如果文件存在错误,decode到m_dic过程中可能会丢弃部分数据,所以要将m_dic,保证m_dic与文件的同步
if (needFullWriteback) {
[self fullWriteback];
}
} else { // 文件不正常且不打算恢复,需要重建,丢弃原来的数据
[self writeAcutalSize:0];
m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
[self recaculateCRCDigest];
}
} else {
// 文件中没有kv,没有必要读入dic
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;
}
网友评论