MMKV实现:数据存储和读取

MMKV实现:数据存储和读取

本文链接:https://blog.csdn.net/feather_wch/article/details/131671190

整体

1、mmkv实现整体思路

  1. mmkv Java层创建了多个MMKV对象(底层同一个native对象)
  2. 底层C层用unordered_map存储C层对象,保证了上层都访问的是同一个mmkv C++对象

2、mmkv方法

  1. 初始化方法-(1)构造map集合(2)创建目录和文件
  2. defaultMMKV(1)获取mmkv对象,底层mmkvWithId map中查找保证唯一性 instance是底层全局的 (2)MMKV()中读取文件,解析文件
  3. mmkv

3、ID是什么?有什么用?

  1. 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

  1. 编码
  2. 检查文件容量->去重->扩容->全量更新
  3. 增量更新

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、为什么双倍扩容?

  1. mmap规则限制:整数倍
  2. 避免频繁扩容

概念

9、protobuf负数编码:

  1. 对负数的编码将int32作为int64处理,负数的变长编码一定是10字节

10、原码、反码、补码是什么

  1. 数字1: 都是0..001
  2. 数字-1: 原码 10..001 反码 11..110 补码 = 反码 + 1
  3. 总结:1存储的数据是0..001,-1存储的数据是 补码:1..111

11、protobuf长度如何计算?

  1. 例如负数int64, 64 / 7 + 1 = 10个字节

Protobuf

编码

12、writeString的实现

  1. 没有特殊编码:写入长度+写入byte数据
  2. 为什么?String本身就是变长编码

1. 写入长度 writeInt32(value.size()) 2. 写入String byte数组 memcpy(xxx, value.data(), size)

13、writeInt32

  1. 负数作为64位int写入
  2. 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. 负数右移,因为负数存储的是补码,例如-1存储的是:111...111
  2. >>= 带符号右移会导致,死循环
  3. 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、实现解码

  1. 负数解码为32位int时需要特殊考虑

16、readInt64()

  1. readByte
  2. 最高位 = 1,再读取1byte
  3. 最高位 = 0,停止读取

浮点数的编码

17、Float在protobuf中采用定长编码为4byte

  1. 需要转为4byte的int32进行处理
  2. float为负数,也是一样处理

18、怎么获得float中每一个字节的数据?

  1. 用int32

19、如何将float转为int32?

  1. 直接转会丢失精度
  2. 方法1:共用体,共享内存 【mmkv中采用,名为Converter】
  3. 方法2:int32_t p = *(int *)&j 浮点数取地址转为int指针,再*取值

20、共用体是什么?

  1. 采用内存共享技术

union X{ int32_t i; float j; }; X x; x.j = 1.1; x.i 就代表 float数据

21、writeFloat实现

  1. 转为int32后,分四次writeByte

22、readFloat实现

  1. readByte四次

23、Java中如何把float转为int?native方法,c++实现

  1. Float.floatToIntBits();
  2. Float.intBitsToFloat();

知识补充

1、C++auto

  1. auto是什么?自动推导类型

2、4 2 1 r w x 可读可写可执行

3、Linux 0777 最前面的0代表什么?

  1. 0代表suid、guid。如果是suid代表调用者拥有所有者权限,guid代表调用者拥有同组用户权限

4、负数为什么要用补码存储?=计算机为什么用补码存储数据?

  1. 简化:补码让加法和减法都可以用加法电路实现,用加法代替减法

猜你喜欢

转载自blog.csdn.net/feather_wch/article/details/131671190
今日推荐