前言:由于知识点多,分了多个记录。
MMKV( 一) 了解原理
MMKV (二)基础知识点和实现流程解析
MMKV (三) POSIX线程和文件锁
负数编码
需要 原码,反码,补码
的基础知识
在Protobuf为了让int32和int64在编码格式上兼容,对负数的编码将int32视为int64处理,因此负数使用Varint(变长)编码一定是10字节。
为什么是10位?
-1 的源码,反码和补码
原码:高位1表示负数------------------------> 10...(省略0)01
反码:符号位不变 ------------------------> 11...(省略1)10
补码:补码=反码+1 并且符号位不变 ------> 11...(省略1)11
所以-1 用 64个1表示 long类型
protobuf 占用:负数:64 /7 = 9 9+1 = 10 存储位
int32
int64
编码的实现
怎么判断int32数据是否只有低7位有1,即此数据只需要一个字节存储?
十进制 | 二进制 | 字节数 |
---|---|---|
1 | 0000 0000 0000 0000 0000 0000 0000 0001 | 1 |
256 | 0000 0000 0000 0000 0000 0001 0000 0000 | 2 |
(i & ~0x7f) == 0,true:i 只在低7位有数据
基础知识右移
-
带符号右移:
符号不变,正数补充0,负数补充1
-
无符号右移:
忽略符号补0
任意数「 X & 0x7f」 都是取低7位数
0xffffffff << 7 -> 1111....1111 1000 0000
0x7f
0000 ... 0000 0111 1111
&
1111....1111 1000 0000 (1111....1111 1000 1000 等等)
所以只要是7位和0x7f 做&运算都是==0
「X | 0x80 」最高为都是1,低七位是元数据
X & ~0x7f ----------通用带符号的低七位
在c 中 ,int8 如何判断最高位是0还是1?
第一种 :直接就判断 x > 0 最高位是0,否则x<0就是1
第二种:或者 x & 0x80 == 0 判断 最高位
常用的编码拼接方式
编码拼接
temp | = newtemp << 7
temp = 1000
newtemp = 1111 1111
拼接 : 1111 1111 000 1000
总结:
X & 0x7f | 0x80
& 0x7f : 获得低7位;
| 0x80: 最高位赋1;
浮点书处理
Float在Protobuf编码中使用定长编码固定为4字节,但是对于Float无法通过位移运算获取每个字节。
int32也为4个字节,所以Float可以转换为int32处理。那如何使用int32表示Float数据?
Java 中 float 用int表示,然后再解码回float
@Test
public void int_convert_float(){
float i = 1.1f;
int j = Float.floatToIntBits(i);
float k = Float.intBitsToFloat(j);
System.out.println(k);
}
核心的实现流程,具体去github搜索mmkv 「by tencent出品」
自定义核心demo 预览
结构demo.png编码考虑的流程
- 编码
- 检查文件容量
- 扩容,按照页的方式存储
MMKV.cpp
类比我们 SharePreference中的putInt功能
void MMKV::putInt(const char *key, int value) {
size_t size = computeInt32Size(value);
//编码value
auto *buffer = new InputBuffer(size);
// 编码!
OutputBuffer buf(buffer->data(),buffer->length());
buf.writeInt32(value);
//记录到内存
auto itr = m_dic.find(key);
if (itr != m_dic.end()) {
delete itr->second;
}
m_dic[key] = buffer;
//同步到映射区(文件)
appendDataWithKey(key, buffer);
}
size_t size = computeInt32Size(value);
计算我们编码多少个字节
PBUtility
size_t computeInt32Size(int32_t value) {
if (value < 0){
return 10;
}
//0xffffffff 表示 32位能表示的 最大值
//<< 7 则低7位变成0 与上value
//如果value只要7位就够了则=0,编码只需要一个字节,否则进入其他判断
if ((value & (0xffffffff << 7)) == 0) {
return 1;
} else if ((value & (0xffffffff << 14)) == 0) {
return 2;
} else if ((value & (0xffffffff << 21)) == 0) {
return 3;
} else if ((value & (0xffffffff << 28)) == 0) {
return 4;
}
return 5;
}
得到 符号类型解码,我们创建buffer保存数据编码Outputbuffer->Inputbuffer
auto 是c++新增语法,不需要new 但是类似
//编码value
auto *buffer = new InputBuffer(size);
// 编码!
OutputBuffer buf(buffer->data(),buffer->length());
buf.writeInt32(value);
appendDataWithKey
扩容或者增量更新
MMKV 中扩容代码
void MMKV::appendDataWithKey(string key, InputBuffer *value) {
//计算保存这个key-value需要多少字节
size_t itemSize = computeItemSize(key, value);
// 空闲空间不够了
if (itemSize > m_output->spaceLeft()){
// 计算去重key后的数据 所需的存储空间
size_t needSize = computeMapSize(m_dic);
needSize += Fixed32Size; //总长度 4字节
//小于文件大小
if (needSize >= m_size){
int32_t oldSize = m_size;
do {
//扩充一倍 为什么??? mmap规则限制:整数倍
m_size *= 2;
} while (needSize >= m_size);
//重新设定文件大小
ftruncate(m_fd, m_size);
//解除映射
munmap(m_ptr, oldSize);
//重新映射
m_ptr = (int8_t *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
}
//全量更新
writeAcutalSize(needSize - Fixed32Size);
delete m_output;
//创建输出
m_output = new OutputBuffer(m_ptr + Fixed32Size,
m_size - Fixed32Size);
//把map写入文件
auto iter = m_dic.begin();
for (; iter != m_dic.end(); iter++) {
auto k = iter->first;
auto v = iter->second;
m_output->writeString(k);
m_output->writeData(v);
}
} else{
//增量更新
writeAcutalSize(m_actualSize + itemSize);
//写入key
m_output->writeString(key);
//写入value
m_output->writeData(value);
}
}
Fixed32Size 4个字节的总长度
保存数据如果大于了剩余空间,需要进行扩容
-
计算去重的所有kv有效长度+ 4 字节
-
由于我们是按照页的存储方式所以 *=2 方式倍数扩容
在MMKV 中有一些复杂的策略算法,可以阅读源代码。 -
然后重新设置文件大小,先把原来老的mmap映射解除掉,然后重新mmap映射完成扩容。完成全量更新覆盖
不大于,增量更新
- 先更新头4个字节,原有长度+kv长度
- 添加kv
涉及到的源代码 github代码
Tecent/MMKV Tecent/MMKV
续接 MMKV(一)续接 MMKV一
网友评论