MMKV--源码学习

kv数据持久化需要的功能

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

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

MMKV号称满足这些特性:

  1. 可靠,实时写入
  2. 高性能
MMKV--源码学习_第1张图片

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

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

MMKV的设计

MMKV--源码学习_第2张图片
MMKV 设计

在内存映射后,操作文件使用指针就可以完成,文件与映射区的同步由内核完成,MMKV维护着一个的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(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);
            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的类型和自定义解码器支持的类型

        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--源码学习)