源碼進(jìn)階:騰訊開源輕量級(jí)緩存 Mmkv 源碼解析
本文轉(zhuǎn)載自微信公眾號(hào)「Android開發(fā)編程」,作者Android開發(fā)編程。轉(zhuǎn)載本文請(qǐng)聯(lián)系A(chǔ)ndroid開發(fā)編程公眾號(hào)。
前言
MMKV本質(zhì)上的定位和sp有點(diǎn)相似,經(jīng)常用于持久化小數(shù)據(jù)的鍵值對(duì);
其速度可以說是當(dāng)前所有同類型中速度最快,性能最優(yōu)的庫(kù);
今天我們就來聊聊;
一、MMKV介紹和簡(jiǎn)單使用
1、什么是mmkv
MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實(shí)現(xiàn),性能高,穩(wěn)定性強(qiáng);
MMKV 基本原理
內(nèi)存準(zhǔn)備:通過 mmap 內(nèi)存映射文件,提供一段可供隨時(shí)寫入的內(nèi)存塊,App 只管往里面寫數(shù)據(jù),由操作系統(tǒng)負(fù)責(zé)將內(nèi)存回寫到文件,不必?fù)?dān)心 crash 導(dǎo)致數(shù)據(jù)丟失;
數(shù)據(jù)組織:數(shù)據(jù)序列化方面我們選用 protobuf 協(xié)議,pb 在性能和空間占用上都有不錯(cuò)的表現(xiàn);
寫入優(yōu)化:考慮到主要使用場(chǎng)景是頻繁地進(jìn)行寫入更新,我們需要有增量更新的能力,考慮將增量 kv 對(duì)象序列化后,append 到內(nèi)存末尾;
空間增長(zhǎng):使用 append 實(shí)現(xiàn)增量更新帶來了一個(gè)新的問題,就是不斷 append 的話,文件大小會(huì)增長(zhǎng)得不可控,我們需要在性能和空間上做個(gè)折中;
2、MMKV的使用
使用前請(qǐng)初始化:
- MMKV.initialize(this)
mmkv寫入鍵值對(duì);
- var mmkv = MMKV.defaultMMKV()
- mmkv.encode("bool",true)
- mmkv.encode("int",1)
- mmkv.encode("String","test")
- mmkv.encode("float",1.0f)
- mmkv.encode("double",1.0)
mmkv除了能夠?qū)懭脒@些基本類型,只要SharePrefences支持的,它也一定能夠支持;
mmkv讀取鍵值對(duì);
- var mmkv = MMKV.defaultMMKV()
- var bo = mmkv.decodeBool("bool")
- Log.e(TAG,"bool:${bo}")
- var i = mmkv.decodeInt("int")
- Log.e(TAG,"int:${i}")
- var s = mmkv.decodeString("String")
- Log.e(TAG,"String:${s}")
- var f = mmkv.decodeFloat("float")
- Log.e(TAG,"float:${f}")
- var d = mmkv.decodeDouble("double")
- Log.e(TAG,"double:$k6zqhab033oa")
每一個(gè)key讀取的數(shù)據(jù)類型就是decodexxx對(duì)應(yīng)的類型名字;
mmkv 刪除鍵值對(duì)和查鍵值對(duì);
- var mmkv = MMKV.defaultMMKV()
- mmkv.removeValueForKey("String")
- mmkv.removeValuesForKeys(arrayOf("int","bool"))
- mmkv.containsKey("String")
能夠刪除單個(gè)key對(duì)應(yīng)的value,也能刪除多個(gè)key分別對(duì)應(yīng)的value;
containsKey判斷mmkv的磁盤緩存中是否存在對(duì)應(yīng)的key;
二、MMKV 源碼解析
1、初始化
通過 MMKV.initialize 方法可以實(shí)現(xiàn) MMKV 的初始化:
- public static String initialize(Context context) {
- String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
- return initialize(root);
- }
它采用了內(nèi)部存儲(chǔ)空間下的 mmkv 文件夾作為根目錄,之后調(diào)用了 initialize 方法;
- public static String initialize(String rootDir) {
- MMKV.rootDir = rootDir;
- jniInitialize(MMKV.rootDir);
- return rootDir;
- }
調(diào)用到了 jniInitialize 這個(gè) Native 方法進(jìn)行 Native 層的初始化:
- extern "C" JNIEXPORT JNICALL void
- Java_com_tencent_mmkv_MMKV_jniInitialize(JNIEnv *env, jobject obj, jstring rootDir) {
- if (!rootDir) {
- return;
- }
- const char *kstr = env->GetStringUTFChars(rootDir, nullptr);
- if (kstr) {
- MMKV::initializeMMKV(kstr);
- env->ReleaseStringUTFChars(rootDir, kstr);
- }
- }
這里通過 MMKV::initializeMMKV 對(duì) MMKV 類進(jìn)行了初始化:
- 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());
- mkPath(path);
- free(path);
- MMKVInfo("root dir: %s", g_rootDir.c_str());
- }
實(shí)際上就是記錄下了 rootDir 并創(chuàng)建對(duì)應(yīng)的根目錄,由于 mkPath 方法創(chuàng)建目錄時(shí)會(huì)修改字符串的內(nèi)容,因此需要復(fù)制一份字符串進(jìn)行;
2、獲取 MMKV 對(duì)象
通過 mmkvWithID 方法可以獲取 MMKV 對(duì)象,它傳入的 mmapID 就對(duì)應(yīng)了 SharedPreferences 中的 name,代表了一個(gè)文件對(duì)應(yīng)的 name,而 relativePath 則對(duì)應(yīng)了一個(gè)相對(duì)根目錄的相對(duì)路徑;
- @Nullable
- public static MMKV mmkvWithID(String mmapID, String relativePath) {
- if (rootDir == null) {
- throw new IllegalStateException("You should Call MMKV.initialize() first.");
- }
- long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, relativePath);
- if (handle == 0) {
- return null;
- }
- return new MMKV(handle);
- }
它調(diào)用到了 getMMKVWithId 這個(gè) Native 方法,并獲取到了一個(gè) handle 構(gòu)造了 Java 層的 MMKV 對(duì)象返回;
Java 層通過持有 Native 層對(duì)象的地址從而與 Native 對(duì)象通信;
- extern "C" JNIEXPORT JNICALL jlong Java_com_tencent_mmkv_MMKV_getMMKVWithID(
- JNIEnv *env, jobject obj, jstring mmapID, jint mode, jstring cryptKey, jstring relativePath) {
- MMKV *kv = nullptr;
- // mmapID 為 null 返回空指針
- if (!mmapID) {
- return (jlong) kv;
- }
- string str = jstring2string(env, mmapID);
- bool done = false;
- // 如果需要進(jìn)行加密,獲取用于加密的 key,最后調(diào)用 MMKV::mmkvWithID
- if (cryptKey) {
- string crypt = jstring2string(env, cryptKey);
- if (crypt.length() > 0) {
- if (relativePath) {
- string path = jstring2string(env, relativePath);
- kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, &path);
- } else {
- kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, nullptr);
- }
- done = true;
- }
- }
- // 如果不需要加密,則調(diào)用 mmkvWithID 不傳入加密 key,表示不進(jìn)行加密
- if (!done) {
- if (relativePath) {
- string path = jstring2string(env, relativePath);
- kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, &path);
- } else {
- kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, nullptr);
- }
- }
- return (jlong) kv;
- }
這里實(shí)際上調(diào)用了 MMKV::mmkvWithID 方法,它根據(jù)是否傳入用于加密的 key 以及是否使用相對(duì)路徑調(diào)用了不同的方法;
- MMKV *MMKV::mmkvWithID(
- const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {
- if (mmapID.empty()) {
- return nullptr;
- }
- // 加鎖
- SCOPEDLOCK(g_instanceLock);
- // 將 mmapID 與 relativePath 結(jié)合生成 mmapKey
- auto mmapKey = mmapedKVKey(mmapID, relativePath);
- // 通過 mmapKey 在 map 中查找對(duì)應(yīng)的 MMKV 對(duì)象并返回
- auto itr = g_instanceDic->find(mmapKey);
- if (itr != g_instanceDic->end()) {
- MMKV *kv = itr->second;
- return kv;
- }
- // 如果找不到,構(gòu)建路徑后構(gòu)建 MMKV 對(duì)象并加入 map
- if (relativePath) {
- auto filePath = mappedKVPathWithID(mmapID, mode, relativePath);
- if (!isFileExist(filePath)) {
- if (!createFile(filePath)) {
- return nullptr;
- }
- }
- MMKVInfo("prepare to load %s (id %s) from relativePath %s", mmapID.c_str(), mmapKey.c_str(),
- relativePath->c_str());
- }
- auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath);
- (*g_instanceDic)[mmapKey] = kv;
- return kv;
- }
這里的步驟如下:
- 通過 mmapedKVKey 方法對(duì) mmapID 及 relativePath 進(jìn)行結(jié)合生成了對(duì)應(yīng)的 mmapKey,它會(huì)將它們兩者的結(jié)合經(jīng)過 md5 從而生成對(duì)應(yīng)的 key,主要目的是為了支持不同相對(duì)路徑下的同名 mmapID;
- 通過 mmapKey 在 g_instanceDic 這個(gè) map 中查找對(duì)應(yīng)的 MMKV 對(duì)象,如果找到直接返回;
- 如果找不到對(duì)應(yīng)的 MMKV 對(duì)象,構(gòu)建一個(gè)新的 MMKV 對(duì)象,加入 map 后返回;
- 構(gòu)造 MMKV 對(duì)象;
MMKV 的構(gòu)造函數(shù):
- MMKV::MMKV(
- const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath)
- : m_mmapID(mmapedKVKey(mmapID, relativePath))
- // ...) {
- // ...
- if (m_isAshmem) {
- m_ashmemFile = new MmapedFile(m_mmapID, static_cast<size_t>(size), MMAP_ASHMEM);
- m_fd = m_ashmemFile->getFd();
- } else {
- m_ashmemFile = nullptr;
- }
- // 通過加密 key 構(gòu)建 AES 加密對(duì)象 AESCrypt
- if (cryptKey && cryptKey->length() > 0) {
- m_crypter = new AESCrypt((const unsigned char *) cryptKey->data(), cryptKey->length());
- }
- // 賦值操作
- // 加鎖后調(diào)用 loadFromFile 加載數(shù)據(jù)
- {
- SCOPEDLOCK(m_sharedProcessLock);
- loadFromFile();
- }
- }
- 進(jìn)行了一些賦值操作,之后如果需要加密則根據(jù)用于加密的 cryptKey 生成對(duì)應(yīng)的 AESCrypt 對(duì)象用于 AES 加密;
- 加鎖后通過 loadFromFile 方法從文件中讀取數(shù)據(jù),這里的鎖是一個(gè)跨進(jìn)程的文件共享鎖;
3、從文件加載數(shù)據(jù)loadFromFile
我們都知道,MMKV 是基于 mmap 實(shí)現(xiàn)的,通過內(nèi)存映射在高效率的同時(shí)保證了數(shù)據(jù)的同步寫入文件,loadFromFile 中就會(huì)真正進(jìn)行內(nèi)存映射:
- void MMKV::loadFromFile() {
- // ...
- // 打開對(duì)應(yīng)的文件
- 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);
- }
- // 將文件大小對(duì)齊到頁大小的整數(shù)倍,用 0 填充不足的部分
- 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);
- }
- // 通過 mmap 將文件映射到內(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", m_mmapID.c_str(),
- m_actualSize, m_size);
- bool loadFromFile = false, needFullWriteback = false;
- if (m_actualSize > 0) {
- if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {
- // 對(duì)文件進(jìn)行 CRC 校驗(yàn),如果失敗根據(jù)策略進(jìn)行不同對(duì)處理
- if (checkFileCRCValid()) {
- loadFromFile = true;
- } else {
- // CRC 校驗(yàn)失敗,如果策略是錯(cuò)誤時(shí)恢復(fù),則繼續(xù)讀取,并且最后需要進(jìn)行回寫
- auto strategic = onMMKVCRCCheckFail(m_mmapID);
- if (strategic == OnErrorRecover) {
- loadFromFile = true;
- needFullWriteback = true;
- }
- }
- } else {
- // 文件大小有誤,若策略是錯(cuò)誤時(shí)恢復(fù),則繼續(xù)讀取,并且最后需要進(jìn)行回寫
- auto strategic = onMMKVFileLengthError(m_mmapID);
- if (strategic == OnErrorRecover) {
- loadFromFile = true;
- needFullWriteback = true;
- }
- }
- }
- // 從文件中讀取內(nèi)容
- if (loadFromFile) {
- MMKVInfo("loading [%s] with crc %u sequence %u", m_mmapID.c_str(),
- m_metaInfo.m_crcDigest, m_metaInfo.m_sequence);
- // 讀取 MMBuffer
- MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
- // 如果需要解密,對(duì)文件進(jìn)行解密
- if (m_crypter) {
- decryptBuffer(*m_crypter, inputBuffer);
- }
- // 通過 MiniPBCoder 將 MMBuffer 轉(zhuǎn)換為 Map
- m_dic.clear();
- MiniPBCoder::decodeMap(m_dic, inputBuffer);
- // 構(gòu)造用于輸出的 CodeOutputData
- 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;
- }
步驟如下:
- 打開文件并獲取文件大小,將文件的大小對(duì)齊到頁的整數(shù)倍,不足則補(bǔ) 0(與內(nèi)存映射的原理有關(guān),內(nèi)存映射是基于頁的換入換出機(jī)制實(shí)現(xiàn)的);
- 通過 mmap 函數(shù)將文件映射到內(nèi)存中,得到指向該區(qū)域的指針 m_ptr;
- 對(duì)文件進(jìn)行長(zhǎng)度校驗(yàn)及 CRC 校驗(yàn)(循環(huán)冗余校驗(yàn),可以校驗(yàn)文件完整性),在失敗的情況下會(huì)根據(jù)當(dāng)前策略進(jìn)行抉擇,如果策略是失敗時(shí)恢復(fù),則繼續(xù)讀取,并且在最后將 map 中的內(nèi)容回寫到文件;
- 通過 m_ptr 構(gòu)造出一塊用于管理 MMKV 映射內(nèi)存的 MMBuffer 對(duì)象,如果需要解密,通過之前構(gòu)造的 AESCrypt 進(jìn)行解密;
- 由于 MMKV 使用了 protobuf 進(jìn)行序列化,通過 MiniPBCoder::decodeMap 方法將 protobuf 轉(zhuǎn)換成對(duì)應(yīng)的 map;
- 構(gòu)造用于輸出的 CodedOutputData 類,如果需要回寫(CRC 校驗(yàn)或文件長(zhǎng)度校驗(yàn)失敗),則調(diào)用 fullWriteback 方法將 map 中的數(shù)據(jù)回寫到文件;
4、數(shù)據(jù)寫入
Java 層的 MMKV 對(duì)象繼承了 SharedPreferences 及 SharedPreferences.Editor 接口并實(shí)現(xiàn)了一系列如 putInt、putLong 的方法用于對(duì)存儲(chǔ)的數(shù)據(jù)進(jìn)行修改;
- @Override
- public Editor putInt(String key, int value) {
- encodeInt(nativeHandle, key, value);
- return this;
- }
它調(diào)用到了 encodeInt 這個(gè) Native 方法:
- extern "C" JNIEXPORT JNICALL jboolean Java_com_tencent_mmkv_MMKV_encodeInt(
- JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint value) {
- MMKV *kv = reinterpret_cast<MMKV *>(handle);
- if (kv && oKey) {
- string key = jstring2string(env, oKey);
- return (jboolean) kv->setInt32(value, key);
- }
- return (jboolean) false;
- }
這里將 Java 層持有的 NativeHandle 轉(zhuǎn)為了對(duì)應(yīng)的 MMKV 對(duì)象,之后調(diào)用了其 setInt32 方法:
- bool MMKV::setInt32(int32_t value, const std::string &key) {
- if (key.empty()) {
- return false;
- }
- // 構(gòu)造值對(duì)應(yīng)的 MMBuffer,通過 CodedOutputData 將其寫入 Buffer
- size_t size = pbInt32Size(value);
- MMBuffer data(size);
- CodedOutputData output(data.getPtr(), size);
- output.writeInt32(value);
- return setDataForKey(std::move(data), key);
- }
- 獲取到了寫入的 value 在 protobuf 中所占據(jù)的大小,之后為其構(gòu)造了對(duì)應(yīng)的 MMBuffer 并將數(shù)據(jù)寫入了這段 Buffer,最后調(diào)用到了 setDataForKey 方法;
- 同時(shí)可以發(fā)現(xiàn) CodedOutputData 是與 Buffer 交互的橋梁,可以通過它實(shí)現(xiàn)向 MMBuffer 中寫入數(shù)據(jù);
- bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) {
- if (data.length() == 0 || key.empty()) {
- return false;
- }
- // 獲取寫鎖
- SCOPEDLOCK(m_lock);
- SCOPEDLOCK(m_exclusiveProcessLock);
- // 確保數(shù)據(jù)已讀入內(nèi)存
- checkLoadData();
- // 將 data 寫入 map 中
- auto itr = m_dic.find(key);
- if (itr == m_dic.end()) {
- itr = m_dic.emplace(key, std::move(data)).first;
- } else {
- itr->second = std::move(data);
- }
- m_hasFullWriteback = false;
- return appendDataWithKey(itr->second, key);
- }
數(shù)據(jù)已讀入內(nèi)存的情況下將 data 寫入了對(duì)應(yīng)的 map,之后調(diào)用了 appendDataWithKey 方法:
- bool MMKV::appendDataWithKey(const MMBuffer &data, const std::string &key) {
- size_t keyLength = key.length();
- // 計(jì)算寫入到映射空間中的 size
- size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength);
- size += data.length() + pbRawVarint32Size((int32_t) data.length());
- // 要寫入,獲取寫鎖
- SCOPEDLOCK(m_exclusiveProcessLock);
- // 確定剩余映射空間足夠
- bool hasEnoughSize = ensureMemorySize(size);
- if (!hasEnoughSize || !isFileValid()) {
- return false;
- }
- if (m_actualSize == 0) {
- auto allData = MiniPBCoder::encodeDataWithObject(m_dic);
- if (allData.length() > 0) {
- if (m_crypter) {
- m_crypter->reset();
- auto ptr = (unsigned char *) allData.getPtr();
- m_crypter->encrypt(ptr, ptr, allData.length());
- }
- writeAcutalSize(allData.length());
- m_output->writeRawData(allData); // note: don't write size of data
- recaculateCRCDigest();
- return true;
- }
- return false;
- } else {
- 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;
- }
- }
- 首先計(jì)算了即將寫入到映射空間的內(nèi)容大小,之后調(diào)用了 ensureMemorySize 方法確保剩余映射空間足夠;
- 如果 m_actualSize 為 0,則會(huì)通過 MiniPBCoder::encodeDataWithObject 將整個(gè) map 轉(zhuǎn)換為對(duì)應(yīng)的 MMBuffer,加密后通過 CodedOutputData 寫入,最后重新計(jì)算 CRC 校驗(yàn)碼。否則會(huì)將 key 和對(duì)應(yīng) data 寫入,最后更新 CRC 校驗(yàn)碼;
- m_actualSize 是位于文件的首部的,因此是否為 0 取決于文件對(duì)應(yīng)位置;
注意的是:由于 protobuf 不支持增量更新,為了避免全量寫入帶來的性能問題,MMKV 在文件中的寫入并不是通過修改文件對(duì)應(yīng)的位置,而是直接在后面 append 一條新的數(shù)據(jù),即使是修改了已存在的 key。而讀取時(shí)只記錄最后一條對(duì)應(yīng) key 的數(shù)據(jù),這樣顯然會(huì)在文件中存在冗余的數(shù)據(jù)。這樣設(shè)計(jì)的原因我認(rèn)為是出于性能的考量,MMKV 中存在著一套內(nèi)存重整機(jī)制用于對(duì)冗余的 key-value 數(shù)據(jù)進(jìn)行處理。它正是在確保內(nèi)存充足時(shí)實(shí)現(xiàn)的;
5、內(nèi)存重整ensureMemorySize
我們接下來看看 ensureMemorySize 是如何確保映射空間是否足夠的:
- bool MMKV::ensureMemorySize(size_t newSize) {
- // ...
- if (newSize >= m_output->spaceLeft()) {
- // 如果內(nèi)存剩余大小不足以寫入,嘗試進(jìn)行內(nèi)存重整,將 map 中的數(shù)據(jù)重新寫入 protobuf 文件
- static const int offset = pbFixed32Size(0);
- MMBuffer data = MiniPBCoder::encodeDataWithObject(m_dic);
- size_t lenNeeded = data.length() + offset + newSize;
- if (m_isAshmem) {
- if (lenNeeded > m_size) {
- MMKVWarning("ashmem %s reach size limit:%zu, consider configure with larger size",
- m_mmapID.c_str(), m_size);
- return false;
- }
- } 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);
- // 如果內(nèi)存重整后仍不足以寫入,則將大小不斷乘2直至足夠?qū)懭?,最后通過 mmap 重新映射文件
- if (lenNeeded >= m_size || (lenNeeded + futureUsage) >= m_size) {
- size_t oldSize = m_size;
- do {
- // double 空間直至足夠
- m_size *= 2;
- } while (lenNeeded + futureUsage >= m_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 = oldSize;
- return false;
- }
- // 用零填充不足部分
- if (!zeroFillFile(m_fd, oldSize, m_size - oldSize)) {
- MMKVError("fail to zeroFile [%s] to size %zu, %s", m_mmapID.c_str(), m_size,
- strerror(errno));
- m_size = oldSize;
- return false;
- }
- // unmap
- if (munmap(m_ptr, oldSize) != 0) {
- MMKVError("fail to munmap [%s], %s", m_mmapID.c_str(), strerror(errno));
- }
- // 重新通過 mmap 映射
- m_ptr = (char *) mmap(m_ptr, 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));
- }
- // check if we fail to make more space
- if (!isFileValid()) {
- MMKVWarning("[%s] file not valid", m_mmapID.c_str());
- return false;
- }
- }
- }
- // 加密數(shù)據(jù)
- if (m_crypter) {
- m_crypter->reset();
- auto ptr = (unsigned char *) data.getPtr();
- m_crypter->encrypt(ptr, ptr, data.length());
- }
- // 重新構(gòu)建并寫入數(shù)據(jù)
- writeAcutalSize(data.length());
- delete m_output;
- m_output = new CodedOutputData(m_ptr + offset, m_size - offset);
- m_output->writeRawData(data);
- recaculateCRCDigest();
- m_hasFullWriteback = true;
- }
- return true;
- }
內(nèi)存重整步驟如下:
- 當(dāng)剩余映射空間不足以寫入需要寫入的內(nèi)容,嘗試進(jìn)行內(nèi)存重整;
- 內(nèi)存重整會(huì)將文件清空,將 map 中的數(shù)據(jù)重新寫入文件,從而去除冗余數(shù)據(jù);
- 若內(nèi)存重整后剩余映射空間仍然不足,不斷將映射空間 double 直到足夠,并用 mmap 重新映射;
6、刪除remove
通過 Java 層 MMKV 的 remove 方法可以實(shí)現(xiàn)刪除操作:
- @Override
- public Editor remove(String key) {
- removeValueForKey(key);
- return this;
- }
它調(diào)用了 removeValueForKey 這個(gè) Native 方法:
- extern "C" JNIEXPORT JNICALL void Java_com_tencent_mmkv_MMKV_removeValueForKey(JNIEnv *env,
- jobject instance,
- jlong handle,
- jstring oKey) {
- MMKV *kv = reinterpret_cast<MMKV *>(handle);
- if (kv && oKey) {
- string key = jstring2string(env, oKey);
- kv->removeValueForKey(key);
- }
- }
這里調(diào)用了 Native 層 MMKV 的 removeValueForKey 方法:
- void MMKV::removeValueForKey(const std::string &key) {
- if (key.empty()) {
- return;
- }
- SCOPEDLOCK(m_lock);
- SCOPEDLOCK(m_exclusiveProcessLock);
- checkLoadData();
- removeDataForKey(key);
- }
它在數(shù)據(jù)讀入內(nèi)存的前提下,調(diào)用了 removeDataForKey 方法:
- bool MMKV::removeDataForKey(const std::string &key) {
- if (key.empty()) {
- return false;
- }
- auto deleteCount = m_dic.erase(key);
- if (deleteCount > 0) {
- m_hasFullWriteback = false;
- static MMBuffer nan(0);
- return appendDataWithKey(nan, key);
- }
- return false;
- }
- 這里實(shí)際上是構(gòu)造了一條 size 為 0 的 MMBuffer 并調(diào)用 appendDataWithKey 將其 append 到 protobuf 文件中,并將 key 對(duì)應(yīng)的內(nèi)容從 map 中刪除;
- 讀取時(shí)發(fā)現(xiàn)它的 size 為 0,則會(huì)認(rèn)為這條數(shù)據(jù)已經(jīng)刪除;
7、讀取
我們通過 getInt、getLong 等操作可以實(shí)現(xiàn)對(duì)數(shù)據(jù)的讀取,我們以 getInt 為例:
- @Override
- public int getInt(String key, int defValue) {
- return decodeInt(nativeHandle, key, defValue);
- }
它調(diào)用到了 decodeInt 這個(gè) Native 方法:
- extern "C" JNIEXPORT JNICALL jint Java_com_tencent_mmkv_MMKV_decodeInt(
- JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint defaultValue) {
- MMKV *kv = reinterpret_cast<MMKV *>(handle);
- if (kv && oKey) {
- string key = jstring2string(env, oKey);
- return (jint) kv->getInt32ForKey(key, defaultValue);
- }
- return defaultValue;
- }
它調(diào)用到了 MMKV.getInt32ForKey 方法:
- int32_t MMKV::getInt32ForKey(const std::string &key, int32_t defaultValue) {
- if (key.empty()) {
- return defaultValue;
- }
- SCOPEDLOCK(m_lock);
- auto &data = getDataForKey(key);
- if (data.length() > 0) {
- CodedInputData input(data.getPtr(), data.length());
- return input.readInt32();
- }
- return defaultValue;
- }
調(diào)用了 getDataForKey 方法獲取到了 key 對(duì)應(yīng)的 MMBuffer,之后通過 CodedInputData 將數(shù)據(jù)讀出并返回;
長(zhǎng)度為 0 時(shí)會(huì)將其視為不存在,返回默認(rèn)值;
- const MMBuffer &MMKV::getDataForKey(const std::string &key) {
- checkLoadData();
- auto itr = m_dic.find(key);
- if (itr != m_dic.end()) {
- return itr->second;
- }
- static MMBuffer nan(0);
- return nan;
- }
這里實(shí)際上是通過在 Map 中尋找從而實(shí)現(xiàn),找不到會(huì)返回 size 為 0 的 Buffer;
MMKV讀寫是直接讀寫到mmap文件映射的內(nèi)存上,繞開了普通讀寫io需要進(jìn)入內(nèi)核,寫到磁盤的過程;
總結(jié)
MMKV使用的注意事項(xiàng)
1.保證每一個(gè)文件存儲(chǔ)的數(shù)據(jù)都比較小,也就說需要把數(shù)據(jù)根據(jù)業(yè)務(wù)線存儲(chǔ)分散。這要就不會(huì)把虛擬內(nèi)存消耗過快;
2.適當(dāng)?shù)臅r(shí)候釋放一部分內(nèi)存數(shù)據(jù),比如在App中監(jiān)聽onTrimMemory方法,在Java內(nèi)存吃緊的情況下進(jìn)行MMKV的trim操作;
3.不需要使用的時(shí)候,最好把MMKV給close掉,甚至調(diào)用exit方法。