美文网首页技术进阶
MMKV的原理与实现(二)

MMKV的原理与实现(二)

作者: PanGeng | 来源:发表于2020-01-09 13:25 被阅读0次

    MMKV的原理与实现(二)

    上一篇讲了MMKV的存储原理以及protobuf编码的规则,并以一个整数的编码规则举例。今天我们就从 MMKV的源码来剖析它具体是怎么实现的。

    上次简单的提了一下页的概念,在Linux中,数据都是以分页的形式保存的,32位系统中,一页就是1024个字节,MMKV在初始化文件的时候,给文件分配了一页的大小,后面根据修改后的数据大小再进行动态扩容,每次翻一倍。

    负数编码

    在Protobuf为了让int32和int64在编码格式上兼容,对负数的编码将int32视为int64处理,因此负数使用Varint(变长)编码一定是10字节。

    <img src="https://img-blog.csdnimg.cn/20191228104935384.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMDkwMDcz,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" style="zoom: 80%;" />

    MMKV的实现

    了解了以上两个概念,就可以撸码了

    java初始化与实例化

    使用MMKV的时候需要调用MMKV.java中的初始化方法,最终都会调用到C++层的jniInitialize()方法,这个类就不多说了。

    值得一提的是,每次操作数据时都是通过defaultMMKV来使用的,但是它最终new出来的一个新的对象:

    public static MMKV defaultMMKV() {
            if (rootDir == null) {
                throw new IllegalStateException("You should Call MMKV.initialize() first.");
            }
            long handle = getDefaultMMKV();
            return new MMKV(handle);
        }
    

    最终都返回了一个新的实例。为什么不是单例呢?因为真正的mmkv对象是存放在C++层的,最后传递了一个handle参数,这个handle就是MMKV对象在内存中的地址(这个稍后看C源码可知,其实在C++层,是用了一个map来保存MMKV的,因为每次文件保存的路径可能不一样,所以不能使用单例)。拿到了这个地址引用以后,就可以把这个地址传递回C++层,在C++拿到MMKV的对象进行操作。

    C++初始化与实例化

    void MMKV::initializeMMKV(const std::string &rootDir) {
        static pthread_once_t once_control = PTHREAD_ONCE_INIT;
        pthread_once(&once_control, initialize);
    
        g_rootDir = rootDir;
        char *path = strdup(g_rootDir.c_str());
        if (path) {
            mkPath(path);
            free(path);
        }
    
        MMKVInfo("root dir: %s", g_rootDir.c_str());
    }
    

    可以看到,这里初始化其实只创建了一个目录,mkpath方法中创建了一个可读可写权限的文件夹。那么怎么获取实例呢? 源码中的defaultMMKV中调用返回了mmkvWithID()函数

    MMKV *MMKV::defaultMMKV(MMKVMode mode, string *cryptKey) {
        return mmkvWithID(DEFAULT_MMAP_ID, DEFAULT_MMAP_SIZE, mode, cryptKey);
    }
    
    MMKV *MMKV::mmkvWithID(
        const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {
    
        if (mmapID.empty()) {
            return nullptr;
        }
        SCOPEDLOCK(g_instanceLock);
        // 根据mmapID获取mmkv在map中的key
        auto mmapKey = mmapedKVKey(mmapID, relativePath);
        // 从map集合中根据key查找MMKV实例
        auto itr = g_instanceDic->find(mmapKey);
        // 如果map中存在这个实例,就直接返回
        if (itr != g_instanceDic->end()) {
            MMKV *kv = itr->second;
            return kv;
        }
        // 省略一些不关键代码
        ...
       
        // 如果不存在,根据mmapID创建一个实例,并保存到map中,下次用直接从map中取到
        auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath);
        (*g_instanceDic)[mmapKey] = kv;
        return kv;
    }
    

    这个函数中传递了一个mmapID, 这个id如果其实相当于SharedPreference中的fileName参数,根据id保存到不同的文件中。获取实例时先判断g_instanceDic集合中是否存在实例,如果存在直接返回,否则创建完成MMKV之后放入g_instanceDic集合中。所以其实在C++层是做了单例处理的。具体细节都在上面注释。当然,获取实例的时候mmapID并不是必传的参数,C++已经为我们设置了一个默认值:

    //默认的mmkv文件
    #define DEFAULT_MMAP_ID "mmkv.default"
    

    在MMKV的构造函数中,可以看到调用了一个loadFromFile函数,这个函数的主要作用就是在初始化的时候,读取MMKV文件,并放入map集合中。

    void MMKV::loadFromFile() {
        // 省略不关键代码
        ... 
        // 打开MMKV文件
        m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
        if (m_fd < 0) {
            // 打开失败 
            MMKVError("fail to open:%s, %s", m_path.c_str(), strerror(errno));
        } else {
            // 获取文件大小
            m_size = 0;
            struct stat st = {0};
            if (fstat(m_fd, &st) != -1) {
                m_size = static_cast<size_t>(st.st_size);
            }
            // round up to (n * pagesize)
            if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
                size_t oldSize = m_size;
                m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
                if (ftruncate(m_fd, m_size) != 0) {
                    MMKVError("fail to truncate [%s] to size %zu, %s", m_mmapID.c_str(), m_size,
                              strerror(errno));
                    m_size = static_cast<size_t>(st.st_size);
                }
                zeroFillFile(m_fd, oldSize, m_size - oldSize);
            }
            // 映射到内存
            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], %s", m_mmapID.c_str(), strerror(errno));
            } else {
                memcpy(&m_actualSize, m_ptr, Fixed32Size);
                MMKVInfo("loading [%s] with %zu size in total, file size is %zu, InterProcess %d",
                         m_mmapID.c_str(), m_actualSize, m_size, m_isInterProcess);
                bool loadFromFile = false, needFullWriteback = false;
                if (m_actualSize > 0) {
                    if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {
                        if (checkFileCRCValid()) {
                            loadFromFile = true;
                        } else {
                            auto strategic = mmkv::onMMKVCRCCheckFail(m_mmapID);
                            if (strategic == OnErrorRecover) {
                                loadFromFile = true;
                                needFullWriteback = true;
                            }
                        }
                    } else {
                        auto strategic = mmkv::onMMKVFileLengthError(m_mmapID);
                        if (strategic == OnErrorRecover) {
                            writeAcutalSize(m_size - Fixed32Size);
                            loadFromFile = true;
                            needFullWriteback = true;
                        }
                    }
                }
                if (loadFromFile) {
                    MMKVInfo("loading [%s] with crc %u sequence %u version %u", m_mmapID.c_str(),
                             m_metaInfo.m_crcDigest, m_metaInfo.m_sequence, m_metaInfo.m_version);
                    MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
                    if (m_crypter) {
                        decryptBuffer(*m_crypter, inputBuffer);
                    }
                    m_dic.clear();
                    MiniPBCoder::decodeMap(m_dic, inputBuffer);
                    m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize,
                                                   m_size - Fixed32Size - m_actualSize);
                    if (needFullWriteback) {
                        fullWriteback();
                    }
                } else {
                    SCOPEDLOCK(m_exclusiveProcessLock);
    
                    if (m_actualSize > 0) {
                        writeAcutalSize(0);
                    }
                    m_output = new CodedOutputData(m_ptr + Fixed32Size, m_size - Fixed32Size);
                    recaculateCRCDigest();
                }
                MMKVInfo("loaded [%s] with %zu values", m_mmapID.c_str(), m_dic.size());
            }
        }
    
        if (!isFileValid()) {
            MMKVWarning("[%s] file not valid", m_mmapID.c_str());
        }
    
        m_needLoadFromFile = false;
    }
    

    编码并写入数据

    // 写入64位整型
    void CodedOutputData::writeInt64(int64_t value) {
        this->writeRawVarint64(value);
    }
    // 写入32位整型,判断是否是正数,如果是正数使用32位编码,否则使用64位编码
    void CodedOutputData::writeInt32(int32_t value) {
        if (value >= 0) {
            this->writeRawVarint32(value);
        } else {
            this->writeRawVarint64(value);
        }
    }
    // 写入32位整型
    void CodedOutputData::writeRawVarint32(int32_t value) {
        while (true) {
            // 判断是否只有前7位是有效数据
            if ((value & ~0x7f) == 0) {
                // 如果是,直接写入文件
                this->writeRawByte(static_cast<uint8_t>(value));
                return;
            } else {
                // 否则取前7位,并在最高位补1.
                this->writeRawByte(static_cast<uint8_t>((value & 0x7F) | 0x80));
                // 将数据右移7位,继续判断
                value = logicalRightShift32(value, 7);
            }
        }
    }
    // 写入64位整型, 原理同上
    void CodedOutputData::writeRawVarint64(int64_t value) {
        while (true) {
            if ((value & ~0x7f) == 0) {
                this->writeRawByte(static_cast<uint8_t>(value));
                return;
            } else {
                this->writeRawByte(static_cast<uint8_t>((value & 0x7f) | 0x80));
                value = logicalRightShift64(value, 7);
            }
        }
    }
    

    文章一开始提到了,负数编码时,为了兼容,将int32视为int64,这里根据正负数来判断写入32位还是64位。好了,下面我们重点来解析writeRawVarint32函数,这里搞明白了,存储其他数据道理都一样了。~ . ~

    我们知道Protobuf编码(<a href="https://blog.csdn.net/qq_22090073/article/details/103703291">不了解的点这里</a>),MMKV这里采用了变长编码,所谓变长编码,就是判断这个数据有多少位,有多少位就写入对应的字节数,不多浪费字节空间。

    大家应该还记的Protobuf编码: 首先判断当前数据是否只有前7位是有效数据,如果是,直接写入文件,否则首位补1,右移7位继续判断。

    那么(value & ~0x7f == 0),怎么解呢

    0x7f 的二进制:
    0111 1111
    
    取反 ~0x7f:
    1000 0000
    
    任意数,与上 ~0x7f:
    0101 0101
    0000 0000
    
    = 
    
    0000 0000
    
    

    这样,任何一个二进制的数取和~0x7f进行与运算,如果结果为 0000 0000, 那么就证明了这个数字只有前7位有数据。不知道大家有没有发现,其实只要判断 value < 0x7f就可以了。条件命中,直接写入文件,结束 掉循环。

    否则,取出最低7位,并在最高位补1 : (value & 0x7F) | 0x80)

    0x7f 的二进制:
    0111 1111
    
    任何一个数字与上0x7f:
    0000 0000 0111 1111 
    &
    1000 1111 0101 0101
    这样就得出来了最低7位:
    0000 0000 0101 0101
    
    0x80的二进制:
    1000 0000
    用最低7位,与0x80进行或运算,就再前面补了1:
    0101 0101 
    |
    1000 0000
    =
    1101 0101
    
    

    这种运算流程是不是很熟悉?对的,这就是第一篇文章提到的protobuf整型编码。只不过是用代码实现出来了。

    下面写入数据的方法就很简单了

    void CodedOutputData::writeRawByte(uint8_t value) {
        //满啦,出错啦
        if (m_position == m_size) {
            MMKVError("m_position: %d, m_size: %zd", m_position, m_size);
            return;
        }
        //将byte放入数组
        m_ptr[m_position++] = value;
    }
    

    MMKV使用了一个游标去记录当前存入的位置,如果这个位置超过了文件的大小,就报错了,否则在m_ptr的下一位去插入这个数据。

    解码并读取数据

    上面讲了MMKV编码的实现,解码又是如何做的勒?

    int32_t CodedInputData::readRawVarint32() {
        // 第一个字节
        int8_t tmp = this->readRawByte();
        if (tmp >= 0) {
            // 如果最高位是0,直接返回
            return tmp;
        }
        int32_t result = tmp & 0x7f;
        // 第二个字节
        if ((tmp = this->readRawByte()) >= 0) {
            // 拼接
            result |= tmp << 7;
        } else {
            // 拼接
            result |= (tmp & 0x7f) << 7;
            // 读取第三个字节
            if ((tmp = this->readRawByte()) >= 0) {
                // 拼接
                result |= tmp << 14;
            } else {
                // 拼接
                result |= (tmp & 0x7f) << 14;
                // 读取第4个字节
                if ((tmp = this->readRawByte()) >= 0) {
                    result |= tmp << 21;
                } else {
                    // 拼接
                    result |= (tmp & 0x7f) << 21;
                    // 读取第五个字节并拼接
                    result |= (tmp = this->readRawByte()) << 28;
                    if (tmp < 0) {
                        // discard upper 32 bits
                        for (int i = 0; i < 5; i++) {
                            // 32位以上。。。
                            if (this->readRawByte() >= 0) {
                                return result;
                            }
                        }
                        MMKVError("InvalidProtocolBuffer malformed varint32");
                    }
                }
            }
        }
        return result;
    }
    

    看着一大堆,很头疼?不要急,我们一步一步来分析。其实主要是负数的处理逻辑:

    首先从内存中读取这个数据,使用8位的int接收, 如果有效数据小于8位,占用一个字节,直接返回,这里都没问题。如果大于8位呢?

    首先取出第一个字节,如果最高位是0 ,直接返回,否则继续读取,将第二个字节左移7位,或运算拼接到result前面,再判断最高位,以此读取。。。

    特殊类型的编码和解码

    这里主要讲一下float和double类型的编解码,Float在Protobuf编码中使用定长编码固定为4字节,但是对于Float无法通过位移运算获取每个字节。

    int32也为4个字节,所以Float可以转换为int32处理。那如何使用int32表示Float数据?我们知道,直接转换是会丢失精度的,那么如何转换的呢?这里有两种方式:

    1. MMKV的做法,共用体Union

      template <typename T, typename P>
      union Converter {
          static_assert(sizeof(T) == sizeof(P), "size not match");
          T first;
          P second;
      };
      
      static inline int32_t Float32ToInt32(float v) {
          Converter<float, int32_t> converter;
          converter.first = v;
          return converter.second;
      }
      
    1. 使用地址引用

      float i = 1.1;
      int32_t j = *(int*) &i;
      

    MMKV存取数据

    存数据:

    bool MMKV::setInt32(int32_t value, const std::string &key) {
        if (key.empty()) {
            return false;
        }
        size_t size = pbInt32Size(value);
        MMBuffer data(size);
        CodedOutputData output(data.getPtr(), size);
        output.writeInt32(value);
        // 最终调用setDataForKey进行存入数据
        return setDataForKey(std::move(data), key);
    }
    
    bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) {
        // 省略不关键代码,保证程序的健壮性。。。 
        ...
        // 这里是存数据逻辑
        auto ret = appendDataWithKey(data, key);
        if (ret) {
            m_dic[key] = std::move(data);
            m_hasFullWriteback = false;
        }
        return ret;
    }
    
    bool MMKV::appendDataWithKey(const MMBuffer &data, const std::string &key) {
        //计算保存这个key-value需要多少字节
        size_t keyLength = key.length();
        size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength);
        size += data.length() + pbRawVarint32Size((int32_t) data.length());
        // 分配内存
        bool hasEnoughSize = ensureMemorySize(size);
        SCOPEDLOCK(m_exclusiveProcessLock);
    
        if (!hasEnoughSize || !isFileValid()) {
            return false;
        }
    
        // 写入数据
        writeAcutalSize(m_actualSize + size);
        m_output->writeString(key);
        m_output->writeData(data); // note: write size of data
    
        auto ptr = (uint8_t *) m_ptr + Fixed32Size + m_actualSize - size;
        if (m_crypter) {
            m_crypter->encrypt(ptr, ptr, size);
        }
        updateCRCDigest(ptr, size, KeepSequence);
        return true;
    }
    
    // 因为代码太多了,这里只展示了扩容相关
    bool MMKV::ensureMemorySize(size_t newSize) {
        ...
        if (newSize >= m_output->spaceLeft() || m_dic.empty()) {
            ...
            } else {
                size_t avgItemSize = lenNeeded / std::max<size_t>(1, m_dic.size());
                size_t futureUsage = avgItemSize * std::max<size_t>(8, (m_dic.size() + 1) / 2);
    
                if (lenNeeded >= m_size || (lenNeeded + futureUsage) >= m_size) {
                    size_t oldSize = m_size;
                    do {
                        // 进行扩容,每次都是上一次大小的2倍
                        m_size *= 2;
                    } while (lenNeeded + futureUsage >= m_size);  
                    
                    ...
        }
        return true;
    }
    

    取数据就很简单了:

    int32_t MMKV::getInt32ForKey(const std::string &key, int32_t defaultValue) {
        if (key.empty()) { //如过key是空的,直接返回默认值
            return defaultValue;
        }
        SCOPEDLOCK(m_lock);
        // 根据 key 获取value
        auto &data = getDataForKey(key);
        if (data.length() > 0) {
            // 这就是上面讲到的读取的逻辑 
            CodedInputData input(data.getPtr(), data.length());
            return input.readInt32();
        }
        return defaultValue;
    }
    

    小结

    以上就是MMKV存取数据的主要流程。也是MMKV的主干,我们已经解读出来了,下一篇讲解MMKV的多线程和跨进程设计。

    菜鸟一枚,现学现卖,如有错误,欢迎指正!

    相关文章

      网友评论

        本文标题:MMKV的原理与实现(二)

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