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数据?我们知道,直接转换是会丢失精度的,那么如何转换的呢?这里有两种方式:
-
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; }
-
使用地址引用
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的多线程和跨进程设计。
菜鸟一枚,现学现卖,如有错误,欢迎指正!
网友评论