本文转载自微信公众号「Android开发编程」,作者Android开发编程。转载本文请联系Android开发编程公众号。
前言
MMKV本质上的定位和sp有点相似,经常用于持久化小数据的键值对;
其速度可以说是当前所有同类型中速度最快,性能最优的库;
今天我们就来聊聊;
一、MMKV介绍和简单使用
1、什么是mmkv
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强;
MMKV 基本原理
内存准备:通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失;
数据组织:数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现;
写入优化:考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力,考虑将增量 kv 对象序列化后,append 到内存末尾;
空间增长:使用 append 实现增量更新带来了一个新的问题,就是不断 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:${d}")
每一个key读取的数据类型就是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 方法可以实现 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 并创建对应的根目录,由于 mkPath 方法创建目录时会修改字符串的内容,因此需要复制一份字符串进行;
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 的构造函数:
- 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 加载数据
- {
- SCOPEDLOCK(m_sharedProcessLock);
- loadFromFile();
- }
- }
- 进行了一些赋值操作,之后如果需要加密则根据用于加密的 cryptKey 生成对应的 AESCrypt 对象用于 AES 加密;
- 加锁后通过 loadFromFile 方法从文件中读取数据,这里的锁是一个跨进程的文件共享锁;
3、从文件加载数据loadFromFile
我们都知道,MMKV 是基于 mmap 实现的,通过内存映射在高效率的同时保证了数据的同步写入文件,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);
- }
- // 将文件大小对齐到页大小的整数倍,用 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 校验失败,如果策略是错误时恢复,则继续读取,并且最后需要进行回写
- auto strategic = onMMKVCRCCheckFail(m_mmapID);
- if (strategic == OnErrorRecover) {
- loadFromFile = true;
- needFullWriteback = true;
- }
- }
- } else {
- // 文件大小有误,若策略是错误时恢复,则继续读取,并且最后需要进行回写
- 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;
- }
步骤如下:
- 打开文件并获取文件大小,将文件的大小对齐到页的整数倍,不足则补 0(与内存映射的原理有关,内存映射是基于页的换入换出机制实现的);
- 通过 mmap 函数将文件映射到内存中,得到指向该区域的指针 m_ptr;
- 对文件进行长度校验及 CRC 校验(循环冗余校验,可以校验文件完整性),在失败的情况下会根据当前策略进行抉择,如果策略是失败时恢复,则继续读取,并且在最后将 map 中的内容回写到文件;
- 通过 m_ptr 构造出一块用于管理 MMKV 映射内存的 MMBuffer 对象,如果需要解密,通过之前构造的 AESCrypt 进行解密;
- 由于 MMKV 使用了 protobuf 进行序列化,通过 MiniPBCoder::decodeMap 方法将 protobuf 转换成对应的 map;
- 构造用于输出的 CodedOutputData 类,如果需要回写(CRC 校验或文件长度校验失败),则调用 fullWriteback 方法将 map 中的数据回写到文件;
4、数据写入
Java 层的 MMKV 对象继承了 SharedPreferences 及 SharedPreferences.Editor 接口并实现了一系列如 putInt、putLong 的方法用于对存储的数据进行修改;
- @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 并将数据写入了这段 Buffer,最后调用到了 setDataForKey 方法;
- 同时可以发现 CodedOutputData 是与 Buffer 交互的桥梁,可以通过它实现向 MMBuffer 中写入数据;
- bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) {
- if (data.length() == 0 || key.empty()) {
- return false;
- }
- // 获取写锁
- SCOPEDLOCK(m_lock);
- SCOPEDLOCK(m_exclusiveProcessLock);
- // 确保数据已读入内存
- 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);
- }
数据已读入内存的情况下将 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 一条新的数据,即使是修改了已存在的 key。而读取时只记录最后一条对应 key 的数据,这样显然会在文件中存在冗余的数据。这样设计的原因我认为是出于性能的考量,MMKV 中存在着一套内存重整机制用于对冗余的 key-value 数据进行处理。它正是在确保内存充足时实现的;
5、内存重整ensureMemorySize
我们接下来看看 ensureMemorySize 是如何确保映射空间是否足够的:
- bool MMKV::ensureMemorySize(size_t newSize) {
- // ...
- if (newSize >= m_output->spaceLeft()) {
- // 如果内存剩余大小不足以写入,尝试进行内存重整,将 map 中的数据重新写入 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;
- }
- }
- }
- // 加密数据
- if (m_crypter) {
- m_crypter->reset();
- auto ptr = (unsigned char *) data.getPtr();
- m_crypter->encrypt(ptr, ptr, data.length());
- }
- // 重新构建并写入数据
- 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 中的数据重新写入文件,从而去除冗余数据;
- 若内存重整后剩余映射空间仍然不足,不断将映射空间 double 直到足够,并用 mmap 重新映射;
6、删除remove
通过 Java 层 MMKV 的 remove 方法可以实现删除操作:
- @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);
- }
它在数据读入内存的前提下,调用了 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 中删除;
- 读取时发现它的 size 为 0,则会认为这条数据已经删除;
7、读取
我们通过 getInt、getLong 等操作可以实现对数据的读取,我们以 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 将数据读出并返回;
长度为 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 中寻找从而实现,找不到会返回 size 为 0 的 Buffer;
MMKV读写是直接读写到mmap文件映射的内存上,绕开了普通读写io需要进入内核,写到磁盘的过程;
总结
MMKV使用的注意事项
1.保证每一个文件存储的数据都比较小,也就说需要把数据根据业务线存储分散。这要就不会把虚拟内存消耗过快;
2.适当的时候释放一部分内存数据,比如在App中监听onTrimMemory方法,在Java内存吃紧的情况下进行MMKV的trim操作;
3.不需要使用的时候,最好把MMKV给close掉,甚至调用exit方法。