MMKV实现:数据存储和读取
本文链接:https://blog.csdn.net/feather_wch/article/details/131671190
整体
1、mmkv实现整体思路
- mmkv Java层创建了多个MMKV对象(底层同一个native对象)
- 底层C层用unordered_map存储C层对象,保证了上层都访问的是同一个mmkv C++对象
2、mmkv方法
- 初始化方法-(1)构造map集合(2)创建目录和文件
- defaultMMKV(1)获取mmkv对象,底层mmkvWithId map中查找保证唯一性 instance是底层全局的 (2)MMKV()中读取文件,解析文件
- mmkv
3、ID是什么?有什么用?
- ID默认为 mmkvdefault 可以理解为文件名
4、MMKV对象结构
MMKV.java long handle = getDefaultMMKV() // long 地址,指针 --- JNI层面: MMKV * kv = MMKV::defaultMMKV() return reinterpret_cast(kv) // long值恢复为MMKV对象 MMKV * kv = reinterpret_cast(native_handle) --- MMKV.cpp defaultMMKV(){ return MMKV() }
实现
5、MMKV构造方法:
MMKV() ->省略 多进程模式相关、进程锁、应用锁相关内容 ->loadFromFile() -> fd = open(xxx) -> fstat(xxx) 读取文件大小,判断是否符合页大小倍数 -> mmap 映射 -> memcpy(&actual_size, ptr, Fixed32Size) 头部 4byte 是总长度 => 进行解析到actual_size中 -> while(CodedInputData) // 解析数据保存到Map中 文件解析: 1. MMKV中是Coded Input/Output Data制作的protobuf解析器 2. 自制InputBuffer和OutputBuffer实现Protobuf解析器 ->跳过文件前4byte ->while() ->解析key长度,解析key ->解析value长度,解析value => mmkv不管数据的类型,使用者获取时 getInt/Float自己确定类型 -> value解析为解析器 InputBuffer *value = xxx => 用户去获取时再解析 -> 解析数据长度,包装为InputBuffer ->map中存放(新数据 覆盖 老数据)=> 解决更新问题 -> 清理老数据 -> value = new InputBuffer() 占据了太多内存,需要清理(没人帮我们释放) -> 存放新数据
6、数据获取:getInt
-> m_dic.find(key)
-> InputBuffer * buf //拿到value的解析器
-> value = buf->readInt32() // 解析出int
7、数据存储:putInt
- 编码
- 检查文件容量->去重->扩容->全量更新
- 增量更新
putInt() -> size_t = computeInt32Size() // 按照protobuf编码算出大小 -> 负数 return 10(byte) -> value & (0xffffffff << 7) => 0xffffff 1000 000 结果 == 0,代表value最多占用7位,return 1(byte) -> value & (0xffffffff << 14) == 0,代表value最多占用14位,return 2(byte) -> value & (0xffffffff << 21) == 0,代表value最多占用21位,return 3(byte) -> value & (0xffffffff << 28) == 0,代表value最多占用28位,return 4(byte) -> return 5(byte) -> 编码 OutputBuffer.writeInt32(value) -> 存储到map(内存中): m_dic[key] = buffer -> appendDataWithKey(key, value) //同步到mmap映射文件中 -> computeItemSize(key, value) // 计算保存key-value需要多少空间 -> 检查文件容量 itemSize > spaceLeft() -> key去重(容量不够时) -> 扩容 ftruncate(mSize * 2) // 双倍扩容 -> munmap 解除映射 -> mmap 映射 -> 全量更新 -> map数据写入到文件中(遍历) -> 增量更新
8、为什么双倍扩容?
- mmap规则限制:整数倍
- 避免频繁扩容
概念
9、protobuf负数编码:
- 对负数的编码将int32作为int64处理,负数的变长编码一定是10字节
10、原码、反码、补码是什么
- 数字1: 都是0..001
- 数字-1: 原码 10..001 反码 11..110 补码 = 反码 + 1
- 总结:1存储的数据是0..001,-1存储的数据是 补码:1..111
11、protobuf长度如何计算?
- 例如负数int64, 64 / 7 + 1 = 10个字节
Protobuf
编码
12、writeString的实现
- 没有特殊编码:写入长度+写入byte数据
- 为什么?String本身就是变长编码
1. 写入长度 writeInt32(value.size()) 2. 写入String byte数组 memcpy(xxx, value.data(), size)
13、writeInt32
- 负数作为64位int写入
- while()循环中,每7bit写入
->value < 0 => writeInt64(value) // 负数作为64 int 写入 ->while() ->if(value < 0x7f) ->writeByte(value) ->else ->writeByte(value & 0x7f | 0x80) //取低7位,在最高位赋1(标记位) ->value >>= 7 //每次处理七位数
14、writeInt64实现
- 负数右移,因为负数存储的是补码,例如-1存储的是:111...111
- >>= 带符号右移会导致,死循环
- uint64_t i = value; // 转为无符号数,进行无符号右移
【uint64_t u_value = value; // 转为无符号数,进行无符号右移】 while(true) ->if(u_value & ~0x7f == 0) ->writeByte ->else ->writeByte(u_value & 0x7f | 0x80) ->u_value >>= 7 // 有符号的负数会导致无限循环
解码
15、实现解码
- 负数解码为32位int时需要特殊考虑
16、readInt64()
- readByte
- 最高位 = 1,再读取1byte
- 最高位 = 0,停止读取
浮点数的编码
17、Float在protobuf中采用定长编码为4byte
- 需要转为4byte的int32进行处理
- float为负数,也是一样处理
18、怎么获得float中每一个字节的数据?
- 用int32
19、如何将float转为int32?
- 直接转会丢失精度
- 方法1:共用体,共享内存 【mmkv中采用,名为Converter】
- 方法2:int32_t p = *(int *)&j 浮点数取地址转为int指针,再*取值
20、共用体是什么?
- 采用内存共享技术
union X{ int32_t i; float j; }; X x; x.j = 1.1; x.i 就代表 float数据
21、writeFloat实现
- 转为int32后,分四次writeByte
22、readFloat实现
- readByte四次
23、Java中如何把float转为int?native方法,c++实现
- Float.floatToIntBits();
- Float.intBitsToFloat();
知识补充
1、C++auto
- auto是什么?自动推导类型
2、4 2 1 r w x 可读可写可执行
3、Linux 0777 最前面的0代表什么?
- 0代表suid、guid。如果是suid代表调用者拥有所有者权限,guid代表调用者拥有同组用户权限
4、负数为什么要用补码存储?=计算机为什么用补码存储数据?
- 简化:补码让加法和减法都可以用加法电路实现,用加法代替减法