MMKV--源码学习

作者: sunshinfight | 来源:发表于2019-03-04 01:38 被阅读0次

    kv数据持久化需要的功能

    假设要设计一个kv的存储功能:

    1. 首先是可靠性,在各种情况下能够将kv保存
    2. 性能的要求,当时是越快越好,存储占用的越少越好

    MMKV号称满足这些特性:

    1. 可靠,实时写入
    2. 高性能

    如果撇去高可靠性,可以采取内存缓冲的模式,例如先存入dic,然后在合适的时间同步到文件。这种方式考虑同步的时机是一方面,而且在crash时可能dic未同步到文件。

    如果撇如高性能,可以采用直接的读写文件,例如采用增量式的编码,将kv写入文件,面临的问题也很明显,就是频繁的磁盘io,效率是很低的。

    MMKV的设计

    MMKV 设计

    在内存映射后,操作文件使用指针就可以完成,文件与映射区的同步由内核完成,MMKV维护着一个<String, AnyObject>的dic,在写时同时写入dic和映射区,也就是同时写入dic和文件,所以dic和持久化的数据是同步的,既然是同步的,所以读时直接取dic中的值就好了。

    下面对基本流程的总结:

    1. 内存映射 mmap
    2. crc校验
    3. aes加密
    4. 线程安全
    5. 内存警告

    mmap

    有关mmap相关的知识和使用可以看这里。对于常用kv存储来说,兼顾性能和可靠性

    所以由mmap的相关知识和MMKV的设计可以猜想,MMKV使用mmap要做什么事情:

    1. 映射文件到内存,保存映射区的指针,方便写操作(定义了MiniCodedOutputData实现了对data按字节拷贝到指定区域内存)
    2. 从映射区为dic初始化,方便读操作

    mmap在MMKV中的使用:

    //  MMKV.mm
    
    - (void)loadFromFile {
        m_fd = open(m_path.UTF8String, O_RDWR, S_IRWXU);    // open  得到文件描述符m_fd
        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: 映射内存,获取内存中的指针m_ptr
            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) {
                            inputBuffer = decryptBuffer(*m_cryptor, inputBuffer);
                        }
                        // 2. 初始化m_dic
                        //  如果文件存在错误(例如crc校验不通过),会导致数据错误或是丢失
                        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);
                        // 如果文件存在错误,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;
    }
    

    写入

    为了保证性能,采用增量写入的方式,这需要编解码支持,MMKV的实现了一套增量编解码方案。增量编码是基于性能的考虑,不用将m_dic中的数据全部写回。

    所以写入要实现的功能:

    1. 将kv写入m_dic
    2. 检查文件剩余空间是否够,不够的话按照一定的策略分配(分配策略会选择牺牲少量磁盘空间换取效率,并且会整理kv防止占用过大存储空间),将kv写入内存映射区域,保持两者同步

    在iOS的中,当app进入后台后,内存可能会被swap出,提供给活跃的app,这样会降低效率(因为要再换进内存呀),MMKV提供了后台写保护的功能(基于性能考虑):

    // MMKV.mm
    
    /// 提供对映射内存的保护,防止被系统交换
    
    - (BOOL)protectFromBackgroundWritting:(size_t)size writeBlock:(void (^)(MiniCodedOutputData *output))block {
        if (m_isInBackground) { // 如果在后台,锁定要写入的内存,防止被换出,影响效率
            // 因为mlock的offset是以页为单位的,所以要计算锁定的页偏移
            static const int offset = pbFixed32Size(0);
            static const int pagesize = getpagesize();
            size_t realOffset = offset + m_actualSize - size;
            size_t pageOffset = (realOffset / pagesize) * pagesize;
            size_t pointerOffset = realOffset - pageOffset;
            size_t mmapSize = offset + m_actualSize - pageOffset;
            char *ptr = m_ptr + pageOffset;
            // 锁定要写入的内存区域
            if (mlock(ptr, mmapSize) != 0) {
                MMKVError(@"fail to mlock [%@], %s", m_mmapID, strerror(errno));
                // just fail on this condition, otherwise app will crash anyway
                //block(m_output);
                return NO;
            } else {
                @try {
                    MiniCodedOutputData output(ptr + pointerOffset, size);
                    block(&output);
                    m_output->seek(size);
                } @catch (NSException *exception) {
                    MMKVError(@"%@", exception);
                    return NO;
                } @finally {
                    munlock(ptr, mmapSize);
                }
            }
        } else {
            block(m_output);    // 未在后台,不需要锁定
        }
    
        return YES;
    }
    
    // MMKV.mm 
    // 检查文件剩余空间是否够,不够的话按照一定的策略分配
    
    - (BOOL)ensureMemorySize:(size_t)newSize {
        [self checkLoadData];
    
        if (![self isFileValid]) {
            MMKVWarning(@"[%@] file not valid", m_mmapID);
            return NO;
        }
    
        if (newSize >= m_output->spaceLeft()) {
            // 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<size_t>(1, m_dic.count);
            // 将要使用的空间,持续扩容一半直到足够,并在扩容后,重新映射
            size_t futureUsage = avgItemSize * std::max<size_t>(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);
                MMKVInfo(@"extending [%@] file size from %zu to %zu, incoming size:%zu, futrue usage:%zu",
                         m_mmapID, oldSize, m_size, newSize, futureUsage);
    
                // if we can't extend size, rollback to old state
                if (ftruncate(m_fd, m_size) != 0) { //  扩充文件
                    MMKVError(@"fail to truncate [%@] to size %zu, %s", m_mmapID, m_size, strerror(errno));
                    m_size = oldSize;
                    return NO;
                }
                
                // 文件大小变了,所以要重新映射,先关闭原来的
                if (munmap(m_ptr, oldSize) != 0) {
                    MMKVError(@"fail to munmap [%@], %s", m_mmapID, strerror(errno));
                }
                
                // 从老位置开始映射,因为可能系统没把这块内存分配出去,可能效率会高一些,没有找到证明此写法的详细资料
                m_ptr = (char *) mmap(m_ptr, 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));
                }
    
                // check if we fail to make more space
                if (![self isFileValid]) {
                    MMKVWarning(@"[%@] file not valid", m_mmapID);
                    return NO;
                }
                // keep m_output consistent with m_ptr -- writeAcutalSize: may fail
                delete m_output;
                m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
                m_output->seek(m_actualSize);
            }
    
            // 加密
            if (m_cryptor) {
                m_cryptor->reset();
                auto ptr = (unsigned char *) data.bytes;
                m_cryptor->encrypt(ptr, ptr, data.length);
            }
    
            if ([self writeAcutalSize:data.length] == NO) {
                return NO;
            }
    
            delete m_output;
            m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
            BOOL ret = [self protectFromBackgroundWritting:m_actualSize // 全量写回,实现kv的排重
                                                writeBlock:^(MiniCodedOutputData *output) {
                                                    output->writeRawData(data);
                                                }];
            if (ret) {
                [self recaculateCRCDigest];
            }
            return ret;
        }
        return YES;
    }
    
    
    // MMKV.mm 
    // 2. 检查文件剩余空间是否够,不够的话按照一定的策略分配,将kv写入内存映射区域,保持两者同步
    - (BOOL)appendData:(NSData *)data forKey:(NSString *)key {
        size_t keyLength = [key lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
        size_t 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;
        }
        // 文件是空的,全量写入,case与编码方式相关
        if (m_actualSize == 0) {
            NSData *allData = [MiniPBCoder encodeDataWithObject:m_dic];
            if (allData.length > 0) {
                if (m_cryptor) {
                    m_cryptor->reset();
                    auto ptr = (unsigned char *) allData.bytes;
                    m_cryptor->encrypt(ptr, ptr, allData.length);
                }
                BOOL ret = [self writeAcutalSize:allData.length];
                if (ret) {
                    ret = [self protectFromBackgroundWritting:m_actualSize
                                                   writeBlock:^(MiniCodedOutputData *output) {
                                                       output->writeRawData(allData); // note: don't write size of data
                                                   }];
                    if (ret) {
                        [self recaculateCRCDigest];
                    }
                }
                return ret;
            }
            return NO;
        } else {    // case与编码方式相关,增量写入
            BOOL ret = [self writeAcutalSize:m_actualSize + size];
            if (ret) {
                static const int offset = pbFixed32Size(0);
                ret = [self protectFromBackgroundWritting:size
                                               writeBlock:^(MiniCodedOutputData *output) {
                                                   output->writeString(key);
                                                   output->writeData(data); // note: write size of data
                                               }];
                if (ret) {
                    auto ptr = (uint8_t *) m_ptr + offset + m_actualSize - size;
                    if (m_cryptor) {    // 这里是在写入内存映射区后才做的加密,因为写入的data加入了其他需要的bit(data长度)
                        m_cryptor->encrypt(ptr, ptr, size);
                    }
                    [self updateCRCDigest:ptr withSize:size];
                }
            }
            return ret;
        }
    }
    
    // MMKV.mm 
    // 写入方法
    - (BOOL)setRawData:(NSData *)data forKey:(NSString *)key {
        if (data.length <= 0 || key.length <= 0) {
            return NO;
        }
        CScopedLock lock(m_lock);
    
        [m_dic setObject:data forKey:key];  // 1. 写入m_dic
        m_hasFullWriteBack = NO;
    
        return [self appendData:data forKey:key];   // 2. 写入文件
    }
    

    读:

    因为m_dic已经保证与文件同步了,所以直接读m_dic就可以了,在需要读数据时进行解码,所以在读时是要明确知道数据类型,如果搞错了,行为是不确定的

    // MMKV.mm
    
    // 从m_dic中获取value(NSData类型)
    - (NSData *)getRawDataForKey:(NSString *)key {
        CScopedLock lock(m_lock);
        [self checkLoadData];
        return [m_dic objectForKey:key];
    }
    
    - (id)getObjectOfClass:(Class)cls forKey:(NSString *)key {
        if (key.length <= 0) {
            return nil;
        }
        NSData *data = [self getRawDataForKey:key]; // 从获取data
        if (data.length > 0) {  // 解码, 支持NSObject<NSCoding>的类型和自定义解码器支持的类型
    
            if ([MiniPBCoder isMiniPBCoderCompatibleType:cls]) {
                return [MiniPBCoder decodeObjectOfClass:cls fromData:data]; 
            } else {
                if ([cls conformsToProtocol:@protocol(NSCoding)]) {
                    return [NSKeyedUnarchiver unarchiveObjectWithData:data];
                }
            }
        }
        return nil;
    }
    

    crc校验

    对于大文件的写入,可能发生错误的几率较大,所以对保存kv的文件使用crc32进行校验(可靠性),产生crc码也需要保存,但是因为crc比较小,所以发生错误的几率是比较小的,如果crc文件也要校验,那就是个无尽的循环了。在每次映射结束后都会做crc校验。每次写入时要更新crc码。crc码的更新方式有两种:

    1. 重新计算全部数据的crc码
    2. 做增量的crc码计算
    // MMKV
    - (BOOL)checkFileCRCValid {
        if (m_ptr != nullptr && m_ptr != MAP_FAILED) {
            int offset = pbFixed32Size(0);
            m_crcDigest = (uint32_t) crc32(0, (const uint8_t *) m_ptr + offset, (uint32_t) m_actualSize);   // 获取文件的crc码
    
            // for backward compatibility
            if (!isFileExist(m_crcPath)) {
                MMKVInfo(@"crc32 file not found:%@", m_crcPath);
                return YES;
            }
            NSData *oData = [NSData dataWithContentsOfFile:m_crcPath];
            uint32_t crc32 = 0;
            @try {
                MiniCodedInputData input(oData);
                crc32 = input.readFixed32();    // 获取已经记录的crc码
            } @catch (NSException *exception) {
                MMKVError(@"%@", exception);
            }
            if (m_crcDigest == crc32) {
                return YES; // 校验通过
            }
            MMKVError(@"check crc [%@] fail, crc32:%u, m_crcDigest:%u", m_mmapID, crc32, m_crcDigest);
        }
        return NO;
    }
    
    // MMKV.mm
    // 通过增量更新crc码
    - (void)updateCRCDigest:(const uint8_t *)ptr withSize:(size_t)length {
        if (ptr == nullptr) {
            return;
        }
        // 将原来crc码传入,进行增量的crc码计算,第一个参数是原来的crc码,如果原来的crc码为0,则相当于全量
        m_crcDigest = (uint32_t) crc32(m_crcDigest, ptr, (uint32_t) length);    
    
        if (m_crcPtr == nullptr || m_crcPtr == MAP_FAILED) {
            [self prepareCRCFile];
        }
        if (m_crcPtr == nullptr || m_crcPtr == MAP_FAILED) {
            return;
        }
    
        static const size_t bufferLength = pbFixed32Size(0);
        if (m_isInBackground) {
            if (mlock(m_crcPtr, bufferLength) != 0) {
                MMKVError(@"fail to mlock crc [%@]-%p, %d:%s", m_mmapID, m_crcPtr, errno, strerror(errno));
                // just fail on this condition, otherwise app will crash anyway
                return;
            }
        }
    
        @try {
            MiniCodedOutputData output(m_crcPtr, bufferLength);
            output.writeFixed32((int32_t) m_crcDigest);
        } @catch (NSException *exception) {
            MMKVError(@"%@", exception);
        }
        if (m_isInBackground) {
            munlock(m_crcPtr, bufferLength);
        }
    }
    

    aes加密

    MMKV 使用了 AES CFB-128 算法来加密/解密。具体是采用了 OpenSSL(1.1.0i 版)的实现。我们选择 CFB 而不是常见的 CBC 算法,主要是因为 MMKV 使用 append-only 实现插入/更新操作,流式加密算法更加合适。-- 摘自MMKV github wiki

    线程安全

    MMKV是线程安全的

    MMKV使用c++的类初始化和析构的特性定义了ScopedLock(作用域锁):

    class CScopedLock {
        NSRecursiveLock *m_oLock;
    
    public:
        CScopedLock(NSRecursiveLock *oLock) : m_oLock(oLock) { [m_oLock lock]; }    // 初始化时加锁
    
        ~CScopedLock() {    // 析构时解锁
            [m_oLock unlock];
            m_oLock = nil;
        }
    };
    
    /*
    {
        CScopedLock lock(g_instanceLock);
        操作临界资源。。。
    } 超出作用域,调用lock的析构函数,解锁
    */
    
    

    使用了NSRecursiveLock进行加锁,这降低了死锁的风险,但是对性能会有少量的消耗

    1. 对于每个mmkv实例都会放入一个global的dic保存(缓存),来避免每次都要走做初始化,并且为该对象添加了强引用,防止被释放,并添加了g_instanceLock锁来保障,每次dic进行写操作时,加锁保护,而且保证初始化行为线程安全
    2. 在mmkv中的实例变量是临界资源,所以每次都要加锁,这里需要注意的是在多线程的其情况下,close之后再使用,其行为是不确定的,原因如下:
    // 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;
    
    - (void)close {
        CScopedLock g_lock(g_instanceLock);
        CScopedLock lock(m_lock);
        MMKVInfo(@"closing %@", m_mmapID);
    
        [self clearMemoryCache];
    
        // 这里从dic中移除了该实例,所以引用计数会-1,不同线程有不同autoreleasepool,所以可能被释放
        [g_instanceDic removeObjectForKey:m_mmapID];    
    }
    

    内存警告

    因为内存过高肯能会OOM,而且会降低app运行速度(内存交换),所以在内存警告时对内存释放

    // MMKV
    
    // 主要是两个工作:1. 清理内存中m_dic 2. 关闭映射
    - (void)clearMemoryCache {
        CScopedLock lock(m_lock);
    
        if (m_needLoadFromFile) {
            MMKVInfo(@"ignore %@", m_mmapID);
            return;
        }
        m_needLoadFromFile = YES;
    
        [m_dic removeAllObjects];   // 清理m_dic
        m_hasFullWriteBack = NO;
    
        if (m_output != nullptr) {
            delete m_output;
        }
        m_output = nullptr;
    
        if (m_ptr != nullptr && m_ptr != MAP_FAILED) {
            if (munmap(m_ptr, m_size) != 0) {   // 关闭映射
                MMKVError(@"fail to munmap [%@], %s", m_mmapID, strerror(errno));
            }
        }
        m_ptr = nullptr;
    
        if (m_fd >= 0) {
            if (close(m_fd) != 0) { // 关闭文件
                MMKVError(@"fail to close [%@], %s", m_mmapID, strerror(errno));
            }
        }
        m_fd = -1;
        m_size = 0;
        m_actualSize = 0;
    
        if (m_cryptor) {
            m_cryptor->reset();
        }
    }
    

    相关文章

      网友评论

        本文标题:MMKV--源码学习

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