源碼進階:騰訊開源輕量級緩存 Mmkv 源碼解析
本文轉載自微信公眾號「Android開發(fā)編程」,作者Android開發(fā)編程。轉載本文請聯(lián)系Android開發(fā)編程公眾號。
前言
MMKV本質上的定位和sp有點相似,經常用于持久化小數(shù)據的鍵值對;
其速度可以說是當前所有同類型中速度最快,性能最優(yōu)的庫;
今天我們就來聊聊;
一、MMKV介紹和簡單使用
1、什么是mmkv
MMKV 是基于 mmap 內存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實現(xiàn),性能高,穩(wěn)定性強;
MMKV 基本原理
內存準備:通過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往里面寫數(shù)據,由操作系統(tǒng)負責將內存回寫到文件,不必擔心 crash 導致數(shù)據丟失;
數(shù)據組織:數(shù)據序列化方面我們選用 protobuf 協(xié)議,pb 在性能和空間占用上都有不錯的表現(xiàn);
寫入優(yōu)化:考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力,考慮將增量 kv 對象序列化后,append 到內存末尾;
空間增長:使用 append 實現(xiàn)增量更新帶來了一個新的問題,就是不斷 append 的話,文件大小會增長得不可控,我們需要在性能和空間上做個折中;
2、MMKV的使用
使用前請初始化:
- MMKV.initialize(this)
mmkv寫入鍵值對;
- 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除了能夠寫入這些基本類型,只要SharePrefences支持的,它也一定能夠支持;
mmkv讀取鍵值對;
- 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:$owocgsc")
每一個key讀取的數(shù)據類型就是decodexxx對應的類型名字;
mmkv 刪除鍵值對和查鍵值對;
- var mmkv = MMKV.defaultMMKV()
- mmkv.removeValueForKey("String")
- mmkv.removeValuesForKeys(arrayOf("int","bool"))
- mmkv.containsKey("String")
能夠刪除單個key對應的value,也能刪除多個key分別對應的value;
containsKey判斷mmkv的磁盤緩存中是否存在對應的key;
二、MMKV 源碼解析
1、初始化
通過 MMKV.initialize 方法可以實現(xiàn) MMKV 的初始化:
- public static String initialize(Context context) {
- String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
- return initialize(root);
- }
它采用了內部存儲空間下的 mmkv 文件夾作為根目錄,之后調用了 initialize 方法;
- public static String initialize(String rootDir) {
- MMKV.rootDir = rootDir;
- jniInitialize(MMKV.rootDir);
- return rootDir;
- }
調用到了 jniInitialize 這個 Native 方法進行 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 對 MMKV 類進行了初始化:
- 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());
- }
實際上就是記錄下了 rootDir 并創(chuàng)建對應的根目錄,由于 mkPath 方法創(chuàng)建目錄時會修改字符串的內容,因此需要復制一份字符串進行;
2、獲取 MMKV 對象
通過 mmkvWithID 方法可以獲取 MMKV 對象,它傳入的 mmapID 就對應了 SharedPreferences 中的 name,代表了一個文件對應的 name,而 relativePath 則對應了一個相對根目錄的相對路徑;
- @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);
- }
它調用到了 getMMKVWithId 這個 Native 方法,并獲取到了一個 handle 構造了 Java 層的 MMKV 對象返回;
Java 層通過持有 Native 層對象的地址從而與 Native 對象通信;
- 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;
- // 如果需要進行加密,獲取用于加密的 key,最后調用 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;
- }
- }
- // 如果不需要加密,則調用 mmkvWithID 不傳入加密 key,表示不進行加密
- 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;
- }
這里實際上調用了 MMKV::mmkvWithID 方法,它根據是否傳入用于加密的 key 以及是否使用相對路徑調用了不同的方法;
- 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 結合生成 mmapKey
- auto mmapKey = mmapedKVKey(mmapID, relativePath);
- // 通過 mmapKey 在 map 中查找對應的 MMKV 對象并返回
- auto itr = g_instanceDic->find(mmapKey);
- if (itr != g_instanceDic->end()) {
- MMKV *kv = itr->second;
- return kv;
- }
- // 如果找不到,構建路徑后構建 MMKV 對象并加入 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 方法對 mmapID 及 relativePath 進行結合生成了對應的 mmapKey,它會將它們兩者的結合經過 md5 從而生成對應的 key,主要目的是為了支持不同相對路徑下的同名 mmapID;
- 通過 mmapKey 在 g_instanceDic 這個 map 中查找對應的 MMKV 對象,如果找到直接返回;
- 如果找不到對應的 MMKV 對象,構建一個新的 MMKV 對象,加入 map 后返回;
- 構造 MMKV 對象;
MMKV 的構造函數(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 構建 AES 加密對象 AESCrypt
- if (cryptKey && cryptKey->length() > 0) {
- m_crypter = new AESCrypt((const unsigned char *) cryptKey->data(), cryptKey->length());
- }
- // 賦值操作
- // 加鎖后調用 loadFromFile 加載數(shù)據
- {
- SCOPEDLOCK(m_sharedProcessLock);
- loadFromFile();
- }
- }
- 進行了一些賦值操作,之后如果需要加密則根據用于加密的 cryptKey 生成對應的 AESCrypt 對象用于 AES 加密;
- 加鎖后通過 loadFromFile 方法從文件中讀取數(shù)據,這里的鎖是一個跨進程的文件共享鎖;
3、從文件加載數(shù)據loadFromFile
我們都知道,MMKV 是基于 mmap 實現(xiàn)的,通過內存映射在高效率的同時保證了數(shù)據的同步寫入文件,loadFromFile 中就會真正進行內存映射:
- void MMKV::loadFromFile() {
- // ...
- // 打開對應的文件
- 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);
- }
- // 將文件大小對齊到頁大小的整數(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 將文件映射到內存
- 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) {
- // 對文件進行 CRC 校驗,如果失敗根據策略進行不同對處理
- if (checkFileCRCValid()) {
- loadFromFile = true;
- } else {
- // CRC 校驗失敗,如果策略是錯誤時恢復,則繼續(xù)讀取,并且最后需要進行回寫
- auto strategic = onMMKVCRCCheckFail(m_mmapID);
- if (strategic == OnErrorRecover) {
- loadFromFile = true;
- needFullWriteback = true;
- }
- }
- } else {
- // 文件大小有誤,若策略是錯誤時恢復,則繼續(xù)讀取,并且最后需要進行回寫
- auto strategic = onMMKVFileLengthError(m_mmapID);
- if (strategic == OnErrorRecover) {
- loadFromFile = true;
- needFullWriteback = true;
- }
- }
- }
- // 從文件中讀取內容
- 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);
- // 如果需要解密,對文件進行解密
- if (m_crypter) {
- decryptBuffer(*m_crypter, inputBuffer);
- }
- // 通過 MiniPBCoder 將 MMBuffer 轉換為 Map
- m_dic.clear();
- MiniPBCoder::decodeMap(m_dic, inputBuffer);
- // 構造用于輸出的 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;
- }
步驟如下:
- 打開文件并獲取文件大小,將文件的大小對齊到頁的整數(shù)倍,不足則補 0(與內存映射的原理有關,內存映射是基于頁的換入換出機制實現(xiàn)的);
- 通過 mmap 函數(shù)將文件映射到內存中,得到指向該區(qū)域的指針 m_ptr;
- 對文件進行長度校驗及 CRC 校驗(循環(huán)冗余校驗,可以校驗文件完整性),在失敗的情況下會根據當前策略進行抉擇,如果策略是失敗時恢復,則繼續(xù)讀取,并且在最后將 map 中的內容回寫到文件;
- 通過 m_ptr 構造出一塊用于管理 MMKV 映射內存的 MMBuffer 對象,如果需要解密,通過之前構造的 AESCrypt 進行解密;
- 由于 MMKV 使用了 protobuf 進行序列化,通過 MiniPBCoder::decodeMap 方法將 protobuf 轉換成對應的 map;
- 構造用于輸出的 CodedOutputData 類,如果需要回寫(CRC 校驗或文件長度校驗失敗),則調用 fullWriteback 方法將 map 中的數(shù)據回寫到文件;
4、數(shù)據寫入
Java 層的 MMKV 對象繼承了 SharedPreferences 及 SharedPreferences.Editor 接口并實現(xiàn)了一系列如 putInt、putLong 的方法用于對存儲的數(shù)據進行修改;
- @Override
- public Editor putInt(String key, int value) {
- encodeInt(nativeHandle, key, value);
- return this;
- }
它調用到了 encodeInt 這個 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 轉為了對應的 MMKV 對象,之后調用了其 setInt32 方法:
- bool MMKV::setInt32(int32_t value, const std::string &key) {
- if (key.empty()) {
- return false;
- }
- // 構造值對應的 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 中所占據的大小,之后為其構造了對應的 MMBuffer 并將數(shù)據寫入了這段 Buffer,最后調用到了 setDataForKey 方法;
- 同時可以發(fā)現(xiàn) CodedOutputData 是與 Buffer 交互的橋梁,可以通過它實現(xiàn)向 MMBuffer 中寫入數(shù)據;
- bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) {
- if (data.length() == 0 || key.empty()) {
- return false;
- }
- // 獲取寫鎖
- SCOPEDLOCK(m_lock);
- SCOPEDLOCK(m_exclusiveProcessLock);
- // 確保數(shù)據已讀入內存
- 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ù)據已讀入內存的情況下將 data 寫入了對應的 map,之后調用了 appendDataWithKey 方法:
- bool MMKV::appendDataWithKey(const MMBuffer &data, const std::string &key) {
- size_t keyLength = key.length();
- // 計算寫入到映射空間中的 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;
- }
- }
- 首先計算了即將寫入到映射空間的內容大小,之后調用了 ensureMemorySize 方法確保剩余映射空間足夠;
- 如果 m_actualSize 為 0,則會通過 MiniPBCoder::encodeDataWithObject 將整個 map 轉換為對應的 MMBuffer,加密后通過 CodedOutputData 寫入,最后重新計算 CRC 校驗碼。否則會將 key 和對應 data 寫入,最后更新 CRC 校驗碼;
- m_actualSize 是位于文件的首部的,因此是否為 0 取決于文件對應位置;
注意的是:由于 protobuf 不支持增量更新,為了避免全量寫入帶來的性能問題,MMKV 在文件中的寫入并不是通過修改文件對應的位置,而是直接在后面 append 一條新的數(shù)據,即使是修改了已存在的 key。而讀取時只記錄最后一條對應 key 的數(shù)據,這樣顯然會在文件中存在冗余的數(shù)據。這樣設計的原因我認為是出于性能的考量,MMKV 中存在著一套內存重整機制用于對冗余的 key-value 數(shù)據進行處理。它正是在確保內存充足時實現(xiàn)的;
5、內存重整ensureMemorySize
我們接下來看看 ensureMemorySize 是如何確保映射空間是否足夠的:
- bool MMKV::ensureMemorySize(size_t newSize) {
- // ...
- if (newSize >= m_output->spaceLeft()) {
- // 如果內存剩余大小不足以寫入,嘗試進行內存重整,將 map 中的數(shù)據重新寫入 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);
- // 如果內存重整后仍不足以寫入,則將大小不斷乘2直至足夠寫入,最后通過 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ù)據
- if (m_crypter) {
- m_crypter->reset();
- auto ptr = (unsigned char *) data.getPtr();
- m_crypter->encrypt(ptr, ptr, data.length());
- }
- // 重新構建并寫入數(shù)據
- 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;
- }
內存重整步驟如下:
- 當剩余映射空間不足以寫入需要寫入的內容,嘗試進行內存重整;
- 內存重整會將文件清空,將 map 中的數(shù)據重新寫入文件,從而去除冗余數(shù)據;
- 若內存重整后剩余映射空間仍然不足,不斷將映射空間 double 直到足夠,并用 mmap 重新映射;
6、刪除remove
通過 Java 層 MMKV 的 remove 方法可以實現(xiàn)刪除操作:
- @Override
- public Editor remove(String key) {
- removeValueForKey(key);
- return this;
- }
它調用了 removeValueForKey 這個 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);
- }
- }
這里調用了 Native 層 MMKV 的 removeValueForKey 方法:
- void MMKV::removeValueForKey(const std::string &key) {
- if (key.empty()) {
- return;
- }
- SCOPEDLOCK(m_lock);
- SCOPEDLOCK(m_exclusiveProcessLock);
- checkLoadData();
- removeDataForKey(key);
- }
它在數(shù)據讀入內存的前提下,調用了 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;
- }
- 這里實際上是構造了一條 size 為 0 的 MMBuffer 并調用 appendDataWithKey 將其 append 到 protobuf 文件中,并將 key 對應的內容從 map 中刪除;
- 讀取時發(fā)現(xiàn)它的 size 為 0,則會認為這條數(shù)據已經刪除;
7、讀取
我們通過 getInt、getLong 等操作可以實現(xiàn)對數(shù)據的讀取,我們以 getInt 為例:
- @Override
- public int getInt(String key, int defValue) {
- return decodeInt(nativeHandle, key, defValue);
- }
它調用到了 decodeInt 這個 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;
- }
它調用到了 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;
- }
調用了 getDataForKey 方法獲取到了 key 對應的 MMBuffer,之后通過 CodedInputData 將數(shù)據讀出并返回;
長度為 0 時會將其視為不存在,返回默認值;
- 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;
- }
這里實際上是通過在 Map 中尋找從而實現(xiàn),找不到會返回 size 為 0 的 Buffer;
MMKV讀寫是直接讀寫到mmap文件映射的內存上,繞開了普通讀寫io需要進入內核,寫到磁盤的過程;
總結
MMKV使用的注意事項
1.保證每一個文件存儲的數(shù)據都比較小,也就說需要把數(shù)據根據業(yè)務線存儲分散。這要就不會把虛擬內存消耗過快;
2.適當?shù)臅r候釋放一部分內存數(shù)據,比如在App中監(jiān)聽onTrimMemory方法,在Java內存吃緊的情況下進行MMKV的trim操作;
3.不需要使用的時候,最好把MMKV給close掉,甚至調用exit方法。