开源框架

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

2020-01-15  本文已影响0人  风雨彩虹_123

前言

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;
}
上一篇下一篇

猜你喜欢

热点阅读