MMKV的原理与实现(二)

MMKV的原理与实现(二)

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

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

负数编码

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

在这里插入图片描述

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编码(不了解的点这里),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;
    }
    
  2. 使用地址引用

    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的多线程和跨进程设计。

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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿烦大大@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值