前言
- 源碼的位置:
https://github.com/Peakmain/Video_Audio/tree/master/app/src/main/cpp/src - 我的簡書:http://www.reibang.com/u/3ff32f5aea98
- 我的Github:https://github.com/peakmain
Protobuf協(xié)議
什么是Protobuf
- protobuf 是google開源的一個(gè)序列化框架沙咏,類似xml胎源,json塘匣。但是它存儲的方式是二進(jìn)制
- MMKV就是基于protobuf協(xié)議進(jìn)行數(shù)據(jù)存儲
數(shù)據(jù)結(jié)構(gòu)
- 上面的圖片我們可以知道惭等,ProtoBuf存儲結(jié)構(gòu)是總長度->key的長度->key的內(nèi)容->value的長度->value的內(nèi)容...
- 總長度我們可以用int來存儲,也就是4個(gè)字節(jié)進(jìn)行存儲
- key的長度實(shí)際就是字符串的長度(我們定義key只能是字符串)
PortoBuf寫入方式
一個(gè)字節(jié)有8位裸扶,我們將后7位用來保存數(shù)據(jù)咱旱,第一位用來判斷是否還有字節(jié),如果沒有則為0茫多,如果有則為1祈匙。
-
如何判斷當(dāng)前是否還有字節(jié)?
因?yàn)槲覀冎槐4婧?位字節(jié),而7位字節(jié)全是1的是7F夺欲,所以當(dāng)我們的數(shù)大于7F則表示我們還有字節(jié)
image.png 當(dāng)我們的數(shù)據(jù)大于7F,如何存儲跪帝?
我們以5201314數(shù)據(jù)進(jìn)行分析,首先將5201314轉(zhuǎn)成字節(jié)碼0100 1111 0101 1101 1010 0010
1、當(dāng)前數(shù)據(jù)大于7F,我們先取最低的七位些阅,也就是010 0010伞剑,第一位補(bǔ)1,則數(shù)據(jù)是1010 0010寫入文件
2市埋、5201314右移動(dòng)7F黎泣,左邊不足補(bǔ)0,原數(shù)據(jù)則變成000 0000 1001 1110 1011 1011(大于7F)
3缤谎、取出最低七位,1011 1011寫入文件抒倚。
4、5201314右移動(dòng)7F坷澡,左邊不足補(bǔ)0托呕,原數(shù)據(jù)則變成000 0000 0000 0001 0011 1101
5、取出最低七位,1011 1101寫入文件洋访。原數(shù)據(jù)則變成000 0000 0000 0000 0000 0010
6镣陕、這時(shí)候數(shù)據(jù)小于07F,則直接將直接將0000 0010寫入文件姻政,結(jié)束
上述步驟結(jié)束之后拿到數(shù)據(jù)
1呆抑、1010 0010
2、1011 1011
3汁展、1011 1101
4鹊碍、0000 0010
- 既然已經(jīng)將數(shù)據(jù)存儲了,那如何取出數(shù)據(jù)食绿?
1侈咕、我們將0000 0010拼接到1011 1101之前,因?yàn)?011 1101中之后后七位是有效數(shù)據(jù)器紧,所以第一位需要去掉首位耀销,此時(shí)的原數(shù)據(jù)就是0000 0010 011 1101
2、依次推論铲汪,將上面拼好的數(shù)據(jù)放到1011 1011之前熊尉,得到數(shù)據(jù)0000 0010 011 1101 011 1011
3、再將上面拼好的數(shù)據(jù)放到1010 0010之前掌腰,得到數(shù)據(jù)0 0000 0100 1111 0101 1101 1010 0010
4狰住、去除無效位數(shù)0,也就還原了原來的數(shù)據(jù)0100 1111 0101 1101 1010 0010
代碼實(shí)現(xiàn)
上面寫入方式了解之后齿梁,看起來還是挺簡單催植,但是代碼怎么寫呢肮蛹?
- 1、我們現(xiàn)在寫入一個(gè)int的數(shù)據(jù)创南,怎么獲取它的大小?
7F的字節(jié)碼是0111 1111伦忠,也就是說第一位是1就代表需要兩個(gè)字節(jié)來存。因此我們可以讓我們當(dāng)前的value&(0xFFFFFFFF<<7)扰藕,判斷是否等于0缓苛,如果等于0則表示需要一個(gè)字節(jié)就可以
1111 1111 1111 1111 1111 1111 1000 0000 (0xffffffff<<7)
& 0000 0000 0000 0000 0000 0000 0110 1110 (110)
= 0000 0000 0000 0000 0000 0000 0000 0000 0
假設(shè)我們的value現(xiàn)在是150,因?yàn)橹狄呀?jīng)大于0x7F(也就是上述不成立)邓深,這時(shí)候我們需要將value&(0xFFFFFFFF<<14),如果等于0則表示需要2個(gè)字節(jié)
1111 1111 1111 1111 1100 0000 0000 0000 (0xffffffff<<14)
& 0000 0000 0000 0000 0000 0000 1001 0110 (150)
= 0000 0000 0000 0000 0000 0000 1000 0000 0
以此推論未桥,最終我們可以寫出如下代碼
int32_t ProtoBuf::computeInt32Size(int32_t value) {
//0xffffffff 表示 uint 最大值
//<< 7 則低7位變成0 與上value
//如果value只要7位就夠了則=0,編碼只需要一個(gè)字節(jié),否則進(jìn)入其他判斷
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;
}
-
2芥备、我們現(xiàn)在存一個(gè)key和value的數(shù)據(jù)冬耿,應(yīng)該怎么計(jì)算它的大小(也就是下圖的紅框區(qū)域的大小)
image.png
首先key的長度其實(shí)也就是
int32_t keyLength = key.length();
然后保存key的長度+key內(nèi)容的長度:
int32_t size = keyLength + ProtoBuf::computeInt32Size(keyLength);
value的長度+value內(nèi)容的長度
size += value->length() + ProtoBuf::computeInt32Size(value->length());
所以獲取key+value大小的完整代碼
int32_t ProtoBuf::computeItemSize(std::string key, ProtoBuf *value) {
int32_t keyLength = key.length();
// 保存key的長度與key數(shù)據(jù)需要的字節(jié)
int32_t size = keyLength + ProtoBuf::computeInt32Size(keyLength);
// 加上保存value的長度與value數(shù)據(jù)需要的字節(jié)
size += value->length() + ProtoBuf::computeInt32Size(value->length());
return size;
}
- 3、如何寫入數(shù)據(jù)
我們上面分析了寫入方式萌壳,那么我們現(xiàn)在直接假設(shè)寫入的key數(shù)據(jù)的長度是字符串110亦镶,因?yàn)?10小于0x7F所以直接寫入,則直接寫入即可
if (value <= 0x7f) {
writeByte(value);
return;
}
void ProtoBuf::writeByte(int8_t value) {
if (m_position == m_size) {
//滿啦袱瓮,出錯(cuò)啦
return;
}
//將byte放入數(shù)組
m_buf[m_position++] = value;
}
如果key數(shù)據(jù)的長度是字符串150缤骨,因?yàn)榇藭r(shí)大于0x7f,將150轉(zhuǎn)成字符串 1001 1000 尺借,首先記錄低七位绊起,
(value & 0x7F)
將第一位的數(shù)據(jù)變成1,再移除低7位
writeByte((value & 0x7F) | 0x80);
//7位已經(jīng)寫完了,處理更高位的數(shù)據(jù)
value >>= 7;
原理如下
0111 1111 (0x7F)
& 1001 1000 (150)
= 0001 1000
| 1000 0000 (0X80)
= 1001 1000
此時(shí)key的長度已經(jīng)全部寫完燎斩,那key的內(nèi)容怎么寫呢虱歪,其實(shí)也很簡單,直接將key的內(nèi)容拷貝到數(shù)組就可以了
memcpy(m_buf + m_position, data->getBuf(), numberOfBytes);
因此寫入string數(shù)據(jù)的完整代碼可以寫成如下
void ProtoBuf::writeByte(int8_t value) {
if (m_position == m_size) {
//滿啦栅表,出錯(cuò)啦
return;
}
//將byte放入數(shù)組
m_buf[m_position++] = value;
}
void ProtoBuf::writeRawInt(int32_t value) {
while (true) {
//每次處理7位數(shù)據(jù)笋鄙,如果寫入的數(shù)據(jù) <= 0x7f(7位都是1)那么使用7位就可以表示了
if (value <= 0x7f) {
writeByte(value);
return;
} else {
//大于7位,則先記錄低7位怪瓶,并且將最高位置為1
//1萧落、& 0x7F 獲得低7位數(shù)據(jù)
//2、| 0x80 讓最高位變成1洗贰,表示超過1個(gè)字節(jié)記錄整個(gè)數(shù)據(jù)
writeByte((value & 0x7F) | 0x80);
//7位已經(jīng)寫完了找岖,處理更高位的數(shù)據(jù)
value >>= 7;
}
}
}
void ProtoBuf::writeString(std::string value) {
size_t numberOfBytes = value.size();
writeRawInt(numberOfBytes);
memcpy(m_buf + m_position, value.data(), numberOfBytes);
m_position += numberOfBytes;
}
- 4、如何讀取數(shù)據(jù)哆姻?
如果當(dāng)前的最高位宣增,也就是第一位是0玫膀,則表示是一個(gè)字節(jié)矛缨,直接返回就可以
if ((tmp >> 7) == 0) {
return tmp;
}
如果最高位1代表還有數(shù)據(jù),我們首先讀取低7位的數(shù)據(jù)
int32_t result = tmp & 0x7f;
再讀取一個(gè)字節(jié),將后面的讀取到字節(jié)左移7位拼接到上一個(gè)數(shù)據(jù)的低7位
int32_t ProtoBuf::readInt() {
uint8_t tmp = readByte();
//最高1位為0 這個(gè)字節(jié)是一個(gè)有效int箕昭。
if ((tmp >> 7) == 0) {
return tmp;
}
//獲得低7位數(shù)據(jù)
int32_t result = tmp & 0x7f;
int32_t i = 1;
do {
//再讀一個(gè)字節(jié)
tmp = readByte();
if (tmp < 0x80) {
//讀取后一個(gè)字節(jié)左移7位再拼上前一個(gè)數(shù)據(jù)的低7位
result |= tmp << (7 * i);
} else {
result |= (tmp & 0x7f) << (7 * i);
}
i++;
} while (tmp >= 0x80);
return result;
}
int8_t ProtoBuf::readByte() {
if (m_position == m_size) {
return 0;
}
return m_buf[m_position++];
}
至此ProtoBuf的讀取和寫入都已經(jīng)基本差不多了灵妨,我們來看mmap
內(nèi)存映射MMAP
SharedPreferences的弊端
-
SharedPreferences采用的是IO寫入數(shù)據(jù)
image.png - 通信的本質(zhì)借助內(nèi)核
- 左邊的進(jìn)程把數(shù)據(jù)從用戶空間copy到內(nèi)核空間
- 右邊的進(jìn)程把數(shù)據(jù)從內(nèi)核空間copy到用戶空間
頁、頁框落竹、頁表
-
基本概念
- CPU執(zhí)行一個(gè)進(jìn)程的時(shí)候泌霍,都會訪問內(nèi)存
- 但是并不是直接訪問物理內(nèi)存地址,而是通過虛擬地址訪問物理內(nèi)存地址
- 頁:將進(jìn)程分配的虛擬地址空間劃分成的塊述召,對應(yīng)的大小叫做頁面的大小
- 頁框:將物理地址劃分的塊
- 頁表:記錄每一對頁和頁框的映射關(guān)系
-
頁面大小是4k朱转,或者4k的整數(shù)倍
image.png
mmap
-
原理:通過mmap映射文件的一塊到用戶空間,那么現(xiàn)在通過操作mmap返回的指針积暖,就可以操作mmap映射的用戶空間藤为,同時(shí)相當(dāng)于操作文件
image.png 函數(shù)
void * mmap(void *addr, size_t len, int prot, int flags, int fd, off_t of fset);
- 參數(shù)
- add:地址,當(dāng)為NULL的時(shí)候夺刑,由系統(tǒng)分配
- len:內(nèi)存的大小
- prot:
- PROT_EXEC內(nèi)容可以被執(zhí)行缅疟;
- PROT_READ:內(nèi)容可以被讀取遍愿;
- PROT_WRITE:內(nèi)容可以被寫入;
- PROT_NONE:內(nèi)容不可訪問
- flags:MAP_SHARED:共享存淫;MAP_PRIVATE:私用;MAP_ANONYMOUS:匿名映射(不基于文件)沼填,fd傳入-1
- fd:打開文件的句柄
- fset:偏移大小,必須是4k的整數(shù)倍桅咆,一個(gè)物理頁映射是4k
核心代碼的實(shí)現(xiàn)
具體代碼大家可以看我的github:https://github.com/Peakmain/Video_Audio/tree/master/app/src/main/cpp/src/mmkv/MMKV.cpp
-
初始化,代碼很簡單倾哺,主要?jiǎng)?chuàng)建一個(gè)文件夾和創(chuàng)建文件名字為peakmain_mmkv
image.png
int32_t DEFAULT_MMAP_SIZE =getpagesize();
void MMKV::initializeMMKV(const char *path) {
g_rootDir = path;
//創(chuàng)建文件夾
mkdir(g_rootDir.c_str(), 0777);
}
MMKV::MMKV(const char *mmapID) {
m_path = g_rootDir + "/" + mmapID;
loadFromFile();
}
MMKV *MMKV::defaultMMKV() {
MMKV *kv = new MMKV(DEFAULT_MMAP_ID);
return kv;
}
- 打開文件轧邪,并獲取文件的大小
m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
//獲取文件的具體大小
struct stat st = {0};
if (fstat(m_fd, &st) != -1) {
m_size = st.st_size;
}
- 我們需要保證文件的大小是頁的整數(shù)倍,也就是4k的整數(shù)倍,因?yàn)槲募笮”辉黾恿诵吆#敲丛黾拥膬?nèi)容需要被設(shè)置為0
if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
//調(diào)整為4k整數(shù)倍
int32_t oldSize = m_size;
//新的4k整數(shù)倍
m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
if (ftruncate(m_fd, m_size) != 0) {
m_size = st.st_size;
}
//如果文件大小被增加了忌愚, 讓增加這些大小的內(nèi)容變成空
zeroFillFile(m_fd, oldSize, m_size - oldSize);
}
- mmap去映射文件
m_ptr = static_cast<int8_t *>(mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));
- 獲取到文件句柄之后,我們獲取文件原來的大小(上面我們分析了却邓,文件的前四個(gè)字節(jié)為內(nèi)容的總長度)
memcpy(&m_actualSize, m_ptr, 4);
- 如果m_actualSize>0硕糊,代表原來的文件是有值得,那么就需要將原有的數(shù)據(jù)保存hashmap
if (m_actualSize > 0) {
ProtoBuf inputBuffer(m_ptr + 4, m_actualSize);
//清空
map.clear();
//已有的數(shù)據(jù)添加到Map
while (!inputBuffer.isAtEnd()) {
std::string key = inputBuffer.readString();
LOGE("key=%s ", key.c_str());
if (key.length() > 0) {
ProtoBuf *value = inputBuffer.readData();
if (value && value->length() > 0) {
//相當(dāng)于java的Hashmap的add
map.emplace(key, value);
}
}
}
}
m_output = new ProtoBuf(m_ptr + 4 + m_actualSize,
m_size - 4 - m_actualSize);
這里有人可能不懂為什么這里是m_ptr + 4 + m_actualSize腊徙,這里的目的是將我們的buf指向沒有被填寫的位置
- mmap寫入數(shù)據(jù)
1简十、計(jì)算value需要多少個(gè)字節(jié),將寫入到ProtoBuf,再用一個(gè)key為string撬腾,value為ProtoBuf的類似java HashMap的unordered_map去存
void MMKV::putInt(const std::string &key, int32_t value) {
//value需要幾個(gè)字節(jié)
int32_t size = ProtoBuf::computeInt32Size(value);
ProtoBuf *buf = new ProtoBuf(size);
buf->writeRawInt(value);
map.emplace(key, buf);
appendDataWithKey(key, buf);
}
2螟蝙、計(jì)算待寫入數(shù)據(jù)的大小(也就是key+key的長度+value+value的長度),如果當(dāng)前待寫入數(shù)據(jù)的大小大于剩余的空間大小民傻,就需要擴(kuò)大內(nèi)存胰默。如果內(nèi)存足夠則直接放入數(shù)據(jù)即可
void MMKV::appendDataWithKey(std::string key, ProtoBuf *value) {
//待寫入數(shù)據(jù)的大小
int32_t itemSize = ProtoBuf::computeItemSize(key, value);
if (itemSize > m_output->spaceLeft()) {
//內(nèi)存不夠
//計(jì)算map的大小
int32_t needSize = ProtoBuf::computeMapSize(map);
//加上總長度
needSize += 4;
//擴(kuò)容的大小
//計(jì)算每個(gè)item的平均長度
int32_t avgItemSize = needSize / std::max<int32_t>(1, map.size());
int32_t futureUsage = avgItemSize * std::max<int32_t>(8, (map.size() + 1) / 2);
if (needSize + futureUsage >= m_size) {
int32_t oldSize = m_size;
//如果在需要的與將來可能增加的加起來比擴(kuò)容后還要大场斑,繼續(xù)擴(kuò)容
do {
//擴(kuò)充一倍
m_size *= 2;
} while (needSize + futureUsage >= m_size);
//重新設(shè)定文件大小
ftruncate(m_fd, m_size);
zeroFillFile(m_fd, oldSize, m_size - oldSize);
//解除映射
munmap(m_ptr, oldSize);
//重新映射
m_ptr = (int8_t *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
}
m_actualSize = needSize - 4;
memcpy(m_ptr, &m_actualSize, 4);
LOGE("extending full write");
delete m_output;
m_output = new ProtoBuf(m_ptr + 4,
m_size - 4);
auto iter = map.begin();
for (; iter != map.end(); iter++) {
auto k = iter->first;
auto v = iter->second;
m_output->writeString(k);
m_output->writeData(v);
}
} else {
//內(nèi)存夠
m_actualSize += itemSize;
memcpy(m_ptr, &m_actualSize, 4);
m_output->writeString(key);
m_output->writeData(value);
}
- mmap取出數(shù)據(jù),只需要從map中取出數(shù)據(jù)就可以
int32_t MMKV::getInt(std::string key, int32_t defaultValue) {
auto itr = map.find(key);
if (itr != map.end()) {
ProtoBuf *buf = itr->second;
int32_t returnValue = buf->readInt();
//多次讀取牵署,將position還原為0
buf->restore();
return returnValue;
}
return defaultValue;
}