MMKV的原理與實現(xiàn)(二)

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)換的呢?這里有兩種方式:

  1. 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;
    }
    
  1. 使用地址引用

    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)賣,如有錯誤,歡迎指正!

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 本文轉(zhuǎn)自劉明的分享。原文 簡介 什么是 Google Protocol Buffer? 假如您在網(wǎng)上搜索,應該會得...
    那樣風采閱讀 1,715評論 0 1
  • 說到輕量級的數(shù)據(jù)持久化,大家最先想到的就是SharedPreferences(以下簡稱SP)了,SP存儲方式為xm...
    PanGeng閱讀 2,212評論 0 2
  • 前言 之前一直忙于移動端日志SDK Trojan的開源工作,已十分穩(wěn)定地運行在餓了么團隊App中,集成了日志加密和...
    水木飛雪閱讀 25,315評論 16 96
  • 于敬義的老伴前兩年去世了,兩個兒子也成家立業(yè)單過了。偌大一院子房就他一個人每天出出進進。不過,他并沒有感到孤獨...
    華山大姐閱讀 680評論 3 2
  • 由于課程上提供的網(wǎng)站訪問不了,就隨便找了一個網(wǎng)站來爬地址是http://guo.lu(好像是個很文藝的網(wǎng)站啊XD)...
    星罹閱讀 422評論 0 0

友情鏈接更多精彩內(nèi)容