美文网首页开源框架
iOS进阶——微信开源存储框架MMKV(一)

iOS进阶——微信开源存储框架MMKV(一)

作者: 风雨彩虹_123 | 来源:发表于2020-01-15 18:09 被阅读0次

    前言

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

    相关文章

      网友评论

        本文标题:iOS进阶——微信开源存储框架MMKV(一)

        本文链接:https://www.haomeiwen.com/subject/blqtzctx.html