MMKV的原理與實現(xiàn)(二)
上一篇講了MMKV的存儲原理以及protobuf編碼的規(guī)則,并以一個整數(shù)的編碼規(guī)則舉例。今天我們就從 MMKV的源碼來剖析它具體是怎么實現(xiàn)的。
頁
上次簡單的提了一下頁的概念,在Linux中,數(shù)據(jù)都是以分頁的形式保存的,32位系統(tǒng)中,一頁就是1024個字節(jié),MMKV在初始化文件的時候,給文件分配了一頁的大小,后面根據(jù)修改后的數(shù)據(jù)大小再進行動態(tài)擴容,每次翻一倍。
負數(shù)編碼
在Protobuf為了讓int32和int64在編碼格式上兼容,對負數(shù)的編碼將int32視為int64處理,因此負數(shù)使用Varint(變長)編碼一定是10字節(jié)。
<img src="https://img-blog.csdnimg.cn/20191228104935384.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMDkwMDcz,size_16,color_FFFFFF,t_70" alt="在這里插入圖片描述" style="zoom: 80%;" />
MMKV的實現(xiàn)
了解了以上兩個概念,就可以擼碼了
java初始化與實例化
使用MMKV的時候需要調(diào)用MMKV.java中的初始化方法,最終都會調(diào)用到C++層的jniInitialize()方法,這個類就不多說了。
值得一提的是,每次操作數(shù)據(jù)時都是通過defaultMMKV來使用的,但是它最終new出來的一個新的對象:
public static MMKV defaultMMKV() {
if (rootDir == null) {
throw new IllegalStateException("You should Call MMKV.initialize() first.");
}
long handle = getDefaultMMKV();
return new MMKV(handle);
}
最終都返回了一個新的實例。為什么不是單例呢?因為真正的mmkv對象是存放在C++層的,最后傳遞了一個handle參數(shù),這個handle就是MMKV對象在內(nèi)存中的地址(這個稍后看C源碼可知,其實在C++層,是用了一個map來保存MMKV的,因為每次文件保存的路徑可能不一樣,所以不能使用單例)。拿到了這個地址引用以后,就可以把這個地址傳遞回C++層,在C++拿到MMKV的對象進行操作。
C++初始化與實例化
void MMKV::initializeMMKV(const std::string &rootDir) {
static pthread_once_t once_control = PTHREAD_ONCE_INIT;
pthread_once(&once_control, initialize);
g_rootDir = rootDir;
char *path = strdup(g_rootDir.c_str());
if (path) {
mkPath(path);
free(path);
}
MMKVInfo("root dir: %s", g_rootDir.c_str());
}
可以看到,這里初始化其實只創(chuàng)建了一個目錄,mkpath方法中創(chuàng)建了一個可讀可寫權限的文件夾。那么怎么獲取實例呢? 源碼中的defaultMMKV中調(diào)用返回了mmkvWithID()函數(shù)
MMKV *MMKV::defaultMMKV(MMKVMode mode, string *cryptKey) {
return mmkvWithID(DEFAULT_MMAP_ID, DEFAULT_MMAP_SIZE, mode, cryptKey);
}
MMKV *MMKV::mmkvWithID(
const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {
if (mmapID.empty()) {
return nullptr;
}
SCOPEDLOCK(g_instanceLock);
// 根據(jù)mmapID獲取mmkv在map中的key
auto mmapKey = mmapedKVKey(mmapID, relativePath);
// 從map集合中根據(jù)key查找MMKV實例
auto itr = g_instanceDic->find(mmapKey);
// 如果map中存在這個實例,就直接返回
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second;
return kv;
}
// 省略一些不關鍵代碼
...
// 如果不存在,根據(jù)mmapID創(chuàng)建一個實例,并保存到map中,下次用直接從map中取到
auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath);
(*g_instanceDic)[mmapKey] = kv;
return kv;
}
這個函數(shù)中傳遞了一個mmapID, 這個id如果其實相當于SharedPreference中的fileName參數(shù),根據(jù)id保存到不同的文件中。獲取實例時先判斷g_instanceDic集合中是否存在實例,如果存在直接返回,否則創(chuàng)建完成MMKV之后放入g_instanceDic集合中。所以其實在C++層是做了單例處理的。具體細節(jié)都在上面注釋。當然,獲取實例的時候mmapID并不是必傳的參數(shù),C++已經(jīng)為我們設置了一個默認值:
//默認的mmkv文件
#define DEFAULT_MMAP_ID "mmkv.default"
在MMKV的構(gòu)造函數(shù)中,可以看到調(diào)用了一個loadFromFile函數(shù),這個函數(shù)的主要作用就是在初始化的時候,讀取MMKV文件,并放入map集合中。
void MMKV::loadFromFile() {
// 省略不關鍵代碼
...
// 打開MMKV文件
m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
if (m_fd < 0) {
// 打開失敗
MMKVError("fail to open:%s, %s", m_path.c_str(), strerror(errno));
} else {
// 獲取文件大小
m_size = 0;
struct stat st = {0};
if (fstat(m_fd, &st) != -1) {
m_size = static_cast<size_t>(st.st_size);
}
// round up to (n * pagesize)
if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
size_t oldSize = m_size;
m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
if (ftruncate(m_fd, m_size) != 0) {
MMKVError("fail to truncate [%s] to size %zu, %s", m_mmapID.c_str(), m_size,
strerror(errno));
m_size = static_cast<size_t>(st.st_size);
}
zeroFillFile(m_fd, oldSize, m_size - oldSize);
}
// 映射到內(nèi)存
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
MMKVError("fail to mmap [%s], %s", m_mmapID.c_str(), strerror(errno));
} else {
memcpy(&m_actualSize, m_ptr, Fixed32Size);
MMKVInfo("loading [%s] with %zu size in total, file size is %zu, InterProcess %d",
m_mmapID.c_str(), m_actualSize, m_size, m_isInterProcess);
bool loadFromFile = false, needFullWriteback = false;
if (m_actualSize > 0) {
if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {
if (checkFileCRCValid()) {
loadFromFile = true;
} else {
auto strategic = mmkv::onMMKVCRCCheckFail(m_mmapID);
if (strategic == OnErrorRecover) {
loadFromFile = true;
needFullWriteback = true;
}
}
} else {
auto strategic = mmkv::onMMKVFileLengthError(m_mmapID);
if (strategic == OnErrorRecover) {
writeAcutalSize(m_size - Fixed32Size);
loadFromFile = true;
needFullWriteback = true;
}
}
}
if (loadFromFile) {
MMKVInfo("loading [%s] with crc %u sequence %u version %u", m_mmapID.c_str(),
m_metaInfo.m_crcDigest, m_metaInfo.m_sequence, m_metaInfo.m_version);
MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
if (m_crypter) {
decryptBuffer(*m_crypter, inputBuffer);
}
m_dic.clear();
MiniPBCoder::decodeMap(m_dic, inputBuffer);
m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize,
m_size - Fixed32Size - m_actualSize);
if (needFullWriteback) {
fullWriteback();
}
} else {
SCOPEDLOCK(m_exclusiveProcessLock);
if (m_actualSize > 0) {
writeAcutalSize(0);
}
m_output = new CodedOutputData(m_ptr + Fixed32Size, m_size - Fixed32Size);
recaculateCRCDigest();
}
MMKVInfo("loaded [%s] with %zu values", m_mmapID.c_str(), m_dic.size());
}
}
if (!isFileValid()) {
MMKVWarning("[%s] file not valid", m_mmapID.c_str());
}
m_needLoadFromFile = false;
}
編碼并寫入數(shù)據(jù)
// 寫入64位整型
void CodedOutputData::writeInt64(int64_t value) {
this->writeRawVarint64(value);
}
// 寫入32位整型,判斷是否是正數(shù),如果是正數(shù)使用32位編碼,否則使用64位編碼
void CodedOutputData::writeInt32(int32_t value) {
if (value >= 0) {
this->writeRawVarint32(value);
} else {
this->writeRawVarint64(value);
}
}
// 寫入32位整型
void CodedOutputData::writeRawVarint32(int32_t value) {
while (true) {
// 判斷是否只有前7位是有效數(shù)據(jù)
if ((value & ~0x7f) == 0) {
// 如果是,直接寫入文件
this->writeRawByte(static_cast<uint8_t>(value));
return;
} else {
// 否則取前7位,并在最高位補1.
this->writeRawByte(static_cast<uint8_t>((value & 0x7F) | 0x80));
// 將數(shù)據(jù)右移7位,繼續(xù)判斷
value = logicalRightShift32(value, 7);
}
}
}
// 寫入64位整型, 原理同上
void CodedOutputData::writeRawVarint64(int64_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 = logicalRightShift64(value, 7);
}
}
}
文章一開始提到了,負數(shù)編碼時,為了兼容,將int32視為int64,這里根據(jù)正負數(shù)來判斷寫入32位還是64位。好了,下面我們重點來解析writeRawVarint32函數(shù),這里搞明白了,存儲其他數(shù)據(jù)道理都一樣了。~ . ~
我們知道Protobuf編碼(<a >不了解的點這里</a>),MMKV這里采用了變長編碼,所謂變長編碼,就是判斷這個數(shù)據(jù)有多少位,有多少位就寫入對應的字節(jié)數(shù),不多浪費字節(jié)空間。
大家應該還記的Protobuf編碼: 首先判斷當前數(shù)據(jù)是否只有前7位是有效數(shù)據(jù),如果是,直接寫入文件,否則首位補1,右移7位繼續(xù)判斷。
那么(value & ~0x7f == 0),怎么解呢
0x7f 的二進制:
0111 1111
取反 ~0x7f:
1000 0000
任意數(shù),與上 ~0x7f:
0101 0101
0000 0000
=
0000 0000
這樣,任何一個二進制的數(shù)取和~0x7f進行與運算,如果結(jié)果為 0000 0000, 那么就證明了這個數(shù)字只有前7位有數(shù)據(jù)。不知道大家有沒有發(fā)現(xiàn),其實只要判斷 value < 0x7f就可以了。條件命中,直接寫入文件,結(jié)束 掉循環(huán)。
否則,取出最低7位,并在最高位補1 : (value & 0x7F) | 0x80)
0x7f 的二進制:
0111 1111
任何一個數(shù)字與上0x7f:
0000 0000 0111 1111
&
1000 1111 0101 0101
這樣就得出來了最低7位:
0000 0000 0101 0101
0x80的二進制:
1000 0000
用最低7位,與0x80進行或運算,就再前面補了1:
0101 0101
|
1000 0000
=
1101 0101
這種運算流程是不是很熟悉?對的,這就是第一篇文章提到的protobuf整型編碼。只不過是用代碼實現(xiàn)出來了。
下面寫入數(shù)據(jù)的方法就很簡單了
void CodedOutputData::writeRawByte(uint8_t value) {
//滿啦,出錯啦
if (m_position == m_size) {
MMKVError("m_position: %d, m_size: %zd", m_position, m_size);
return;
}
//將byte放入數(shù)組
m_ptr[m_position++] = value;
}
MMKV使用了一個游標去記錄當前存入的位置,如果這個位置超過了文件的大小,就報錯了,否則在m_ptr的下一位去插入這個數(shù)據(jù)。
解碼并讀取數(shù)據(jù)
上面講了MMKV編碼的實現(xiàn),解碼又是如何做的勒?
int32_t CodedInputData::readRawVarint32() {
// 第一個字節(jié)
int8_t tmp = this->readRawByte();
if (tmp >= 0) {
// 如果最高位是0,直接返回
return tmp;
}
int32_t result = tmp & 0x7f;
// 第二個字節(jié)
if ((tmp = this->readRawByte()) >= 0) {
// 拼接
result |= tmp << 7;
} else {
// 拼接
result |= (tmp & 0x7f) << 7;
// 讀取第三個字節(jié)
if ((tmp = this->readRawByte()) >= 0) {
// 拼接
result |= tmp << 14;
} else {
// 拼接
result |= (tmp & 0x7f) << 14;
// 讀取第4個字節(jié)
if ((tmp = this->readRawByte()) >= 0) {
result |= tmp << 21;
} else {
// 拼接
result |= (tmp & 0x7f) << 21;
// 讀取第五個字節(jié)并拼接
result |= (tmp = this->readRawByte()) << 28;
if (tmp < 0) {
// discard upper 32 bits
for (int i = 0; i < 5; i++) {
// 32位以上。。。
if (this->readRawByte() >= 0) {
return result;
}
}
MMKVError("InvalidProtocolBuffer malformed varint32");
}
}
}
}
return result;
}
看著一大堆,很頭疼?不要急,我們一步一步來分析。其實主要是負數(shù)的處理邏輯:
首先從內(nèi)存中讀取這個數(shù)據(jù),使用8位的int接收, 如果有效數(shù)據(jù)小于8位,占用一個字節(jié),直接返回,這里都沒問題。如果大于8位呢?
首先取出第一個字節(jié),如果最高位是0 ,直接返回,否則繼續(xù)讀取,將第二個字節(jié)左移7位,或運算拼接到result前面,再判斷最高位,以此讀取。。。
特殊類型的編碼和解碼
這里主要講一下float和double類型的編解碼,F(xiàn)loat在Protobuf編碼中使用定長編碼固定為4字節(jié),但是對于Float無法通過位移運算獲取每個字節(jié)。
int32也為4個字節(jié),所以Float可以轉(zhuǎn)換為int32處理。那如何使用int32表示Float數(shù)據(jù)?我們知道,直接轉(zhuǎn)換是會丟失精度的,那么如何轉(zhuǎn)換的呢?這里有兩種方式:
-
MMKV的做法,共用體Union
template <typename T, typename P> union Converter { static_assert(sizeof(T) == sizeof(P), "size not match"); T first; P second; }; static inline int32_t Float32ToInt32(float v) { Converter<float, int32_t> converter; converter.first = v; return converter.second; }
-
使用地址引用
float i = 1.1; int32_t j = *(int*) &i;
MMKV存取數(shù)據(jù)
存數(shù)據(jù):
bool MMKV::setInt32(int32_t value, const std::string &key) {
if (key.empty()) {
return false;
}
size_t size = pbInt32Size(value);
MMBuffer data(size);
CodedOutputData output(data.getPtr(), size);
output.writeInt32(value);
// 最終調(diào)用setDataForKey進行存入數(shù)據(jù)
return setDataForKey(std::move(data), key);
}
bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) {
// 省略不關鍵代碼,保證程序的健壯性。。。
...
// 這里是存數(shù)據(jù)邏輯
auto ret = appendDataWithKey(data, key);
if (ret) {
m_dic[key] = std::move(data);
m_hasFullWriteback = false;
}
return ret;
}
bool MMKV::appendDataWithKey(const MMBuffer &data, const std::string &key) {
//計算保存這個key-value需要多少字節(jié)
size_t keyLength = key.length();
size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength);
size += data.length() + pbRawVarint32Size((int32_t) data.length());
// 分配內(nèi)存
bool hasEnoughSize = ensureMemorySize(size);
SCOPEDLOCK(m_exclusiveProcessLock);
if (!hasEnoughSize || !isFileValid()) {
return false;
}
// 寫入數(shù)據(jù)
writeAcutalSize(m_actualSize + size);
m_output->writeString(key);
m_output->writeData(data); // note: write size of data
auto ptr = (uint8_t *) m_ptr + Fixed32Size + m_actualSize - size;
if (m_crypter) {
m_crypter->encrypt(ptr, ptr, size);
}
updateCRCDigest(ptr, size, KeepSequence);
return true;
}
// 因為代碼太多了,這里只展示了擴容相關
bool MMKV::ensureMemorySize(size_t newSize) {
...
if (newSize >= m_output->spaceLeft() || m_dic.empty()) {
...
} else {
size_t avgItemSize = lenNeeded / std::max<size_t>(1, m_dic.size());
size_t futureUsage = avgItemSize * std::max<size_t>(8, (m_dic.size() + 1) / 2);
if (lenNeeded >= m_size || (lenNeeded + futureUsage) >= m_size) {
size_t oldSize = m_size;
do {
// 進行擴容,每次都是上一次大小的2倍
m_size *= 2;
} while (lenNeeded + futureUsage >= m_size);
...
}
return true;
}
取數(shù)據(jù)就很簡單了:
int32_t MMKV::getInt32ForKey(const std::string &key, int32_t defaultValue) {
if (key.empty()) { //如過key是空的,直接返回默認值
return defaultValue;
}
SCOPEDLOCK(m_lock);
// 根據(jù) key 獲取value
auto &data = getDataForKey(key);
if (data.length() > 0) {
// 這就是上面講到的讀取的邏輯
CodedInputData input(data.getPtr(), data.length());
return input.readInt32();
}
return defaultValue;
}
小結(jié)
以上就是MMKV存取數(shù)據(jù)的主要流程。也是MMKV的主干,我們已經(jīng)解讀出來了,下一篇講解MMKV的多線程和跨進程設計。
菜鳥一枚,現(xiàn)學現(xiàn)賣,如有錯誤,歡迎指正!