MMKV 原理剖析1
Posted 不会写代码的丝丽
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MMKV 原理剖析1相关的知识,希望对你有一定的参考价值。
前言
MMKV
内部有很多精妙的设计思想,本文作为第一章仅仅介绍大致架构
我们看下MMKV
优势:
- 序列化的文件体积更小(采用protobuf变长编码实现)
- 多进程感知和同步(主要针对android端,采用文件锁实现同步,文件头部写版本号偏移信息等)
- 写入时IO操作更快(追加写入和
mmap
)
protobuf变长编码
假设我们有一个int
(32位)类型需要存储,那么我们真的有必要4字节进行存储?
假设我们有两个如下变量:
int a0 = 0x1; //对应二进制 0000 0001
int a1 = 0x2 //对应二进制 0000 0010
int a2 = 0xff; 对应二进制 1111 1111
假设我们存储都以上非常小的数据真的需要完整存4字节?以上a0 a1 a2
只需一字节即可满足需求。
在Google
的protobuf
库就采用了变长编码。
其大致思想每一个字节第一位表示后面是否还需要一字节去表示一个数字。
举个例子:
int c0 = 2; //对应二进制 0000 0000 0000 0000 0000 0000 0000 0010
int c1 = 300F; //对应二进制 0000 0000 0000 0000 0000 0001 0010 1100
0x2 在原始存储结构和protobuf存储结构差异:
300 在原始存储结构和protobuf存储结构差异(您看到官方文档可能会看到大小端转化,这里仅仅为了说明思想):
你会发现300这个数字在protobuf中只需要两个字节。
这里你可能会思考对于负数怎么处理?
比如-1
对应的二进制为11111111
这种时候变长编码反而成为一种负担。
对于负数Protobuf
提供了一个Zigtag
编码思想来处理,因为本文重点不是在这所以简单介绍下:
Zigtag算法思想:
假设我们有一个函数f(x)
可以将负数转化为对应正数并且保证负数越小转化为的正数也是越小,再用生产数字进行变长编码即可。接收方收到这个类型的变量进行逆函数
操作即可
举个例子
f(-1)=1
,f(-2)=3
-1会转化为1,然后进行变长编码
-2回转为3,然后在进行变长编码
在protobuf
中使用sint32
和sint64
就是Zigtag
算法,所以合理使用数据类型也是优化哦
如果你想了解更多可参阅下面文献:
- https://www.jianshu.com/p/73c9ed3a4877
- https://developers.google.com/protocol-buffers/docs/encoding
- https://www.cnblogs.com/en-heng/p/5570609.html
在MMKV中的源码中就是使用了对应的protobuf源码实现,下面我简单看下即可
//CodedOutputData.cpp
void CodedOutputData::writeRawVarint32(int32_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 = logicalRightShift32(value, 7);
}
}
}
//CodedInputData.cpp
int32_t CodedInputData::readRawVarint32() {
int8_t tmp = this->readRawByte();
if (tmp >= 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;
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++) {
if (this->readRawByte() >= 0) {
return result;
}
}
throw invalid_argument("InvalidProtocolBuffer malformed varint32");
}
}
}
}
return result;
}
追加写入
在Android
使用原始SP
作为持久化方案时,默认更新一个数据会导致整个文件重写。而MMKV的方案时追缴新的内容在文件末端。
举个例子:
我们本地有一个文件xx.xml
存储数据
假设我们现在需要修改key1
内容为hello
在SP下会完整重写整个文件进行覆盖,这种方式简单粗暴容易管理但是效率很低。
在MMKV下
MMKV在末尾追缴一个同key数据,读取时以后者为准。在多次更新后MMKV会进行文件整理操作。
状态同步
我们假设我们有多个进程使用MMKV,A进程使用MMKV更新数据,B进程进行读取那么是否可以读取最新的数据?
MMKV会在更新时会在文件头部写版本号和有效内容的大小,另一个进程读取数据只需要判断版本号和有效内容,假设版本号不对那么只需要移动文件指针读取新数据即可。
零拷贝
我们读取文件都需要内核拷贝,而MMKV
利用linux mmap
进行文件映射来减少内核拷贝操作。
关于这块可以参考博其他文章:https://fanmingyi.blog.csdn.net/article/details/114762889
进程同步
我们假设A进程和B进程同时写入MMKV文件怎么办?MMKV采用文件锁的方式解决。
很多读者会看到官方说本想pthread_mutex
进行同步但是android是linux阉割版本所以采用文件锁。
这里简单介绍linux多线程知识,假设我们有两个线程A和B,如果有一个互斥量pthread_mutex_t
被线程A持有,
但是线程A突然意外死亡,会导致pthread_mutex_t
不会释放,介于此linux提供了一个相关api解决了这个问题。
代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
using namespace std;
void * childThreadRun(void *arg){
pthread_mutex_lock(&mutex);
cout<<"Child thread is dead"<<endl;
return 0;
}
int main() {
pthread_mutexattr_t arr;
pthread_mutexattr_init(&arr);
//提供一个死亡线程自动释放互斥锁功能
pthread_mutexattr_setrobust(&arr,PTHREAD_MUTEX_ROBUST);
pthread_mutex_init(&mutex,&arr);
pthread_t tid;
pthread_create(&tid,NULL,childThreadRun,NULL);
sleep(3);
int result= pthread_mutex_lock(&mutex);
cout<<"result "<<result<<endl;
std::cout << "Hello, World!" << std::endl;
return 0;
}
在Android中是没有pthread_mutexattr_setrobust这个函数,假设a进程出现这类情况会导致整个MMKV瘫痪
MMKV文档介绍的锁升级是利用文件锁做的一层封装,具体后面在分析把
以上是关于MMKV 原理剖析1的主要内容,如果未能解决你的问题,请参考以下文章