1. MMKV——基于 mmap 的高性能通用 key-value 组件
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。GitHub地址:https://github.com/Tencent/MMKV
2. MMKV 原理
1)内存准备:通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
2)数据组织:数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
3)写入优化:考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
4)空间增长:使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
dependencies {
implementation 'com.tencent:mmkv-static:1.2.2''
}
MMKV 的使用非常简单,所有变更立马生效,无需调用 sync
、apply
。 在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),例如在 Application
里:
public class MyApp extends Application {
private static final String TAG = MyApp.class.getSimpleName();
@Override
public void onCreate() {
super.onCreate();
String rootDir = MMKV.initialize(this);
Log.i(TAG,"mmkv root: " + rootDir);
}
}
1)MMKV 提供一个全局的实例,可以直接使用:
import com.tencent.mmkv.MMKV;
...
MMKV kv = MMKV.defaultMMKV();
kv.encode("bool", true);
System.out.println("bool: " + kv.decodeBool("bool"));
kv.encode("int", Integer.MIN_VALUE);
System.out.println("int: " + kv.decodeInt("int"));
kv.encode("long", Long.MAX_VALUE);
System.out.println("long: " + kv.decodeLong("long"));
kv.encode("float", -3.14f);
System.out.println("float: " + kv.decodeFloat("float"));
kv.encode("double", Double.MIN_VALUE);
System.out.println("double: " + kv.decodeDouble("double"));
kv.encode("string", "Hello from mmkv");
System.out.println("string: " + kv.decodeString("string"));
byte[] bytes = {'m', 'm', 'k', 'v'};
kv.encode("bytes", bytes);
System.out.println("bytes: " + new String(kv.decodeBytes("bytes")));
2)删除与查询
MMKV kv = MMKV.defaultMMKV();
// 移除指定的key
kv.removeValueForKey("bool");
System.out.println("bool: " + kv.decodeBool("bool"));
// 移除一组key
kv.removeValuesForKeys(new String[]{"int", "long"});
System.out.println("allKeys: " + Arrays.toString(kv.allKeys()));
boolean hasBool = kv.containsKey("bool");
3)如果不同业务需要区别存储,也可以单独创建自己的实例:
MMKV mmkv = MMKV.mmkvWithID("MyID");
mmkv.encode("bool", true);
4)如果业务需要多进程访问,那么在初始化的时候加上标志位 MMKV.MULTI_PROCESS_MODE
:
MMKV mmkv = MMKV.mmkvWithID("InterProcessKV", MMKV.MULTI_PROCESS_MODE);
mmkv.encode("bool", true);
1)支持一下Java语言基础类型:
boolean、int、long、float、double、byte[]。
2)支持一下Java类和容器:
String、Set
1) MMKV 提供了 importFromSharedPreferences()
函数,可以比较方便地迁移数据过来。
2)MMKV 还额外实现了一遍 SharedPreferences
、SharedPreferences.Editor
这两个 interface,在迁移的时候只需两三行代码即可,其他 CRUD 操作代码都不用改。
private void testImportSharedPreferences() {
//SharedPreferences preferences = getSharedPreferences("myData", MODE_PRIVATE);
MMKV preferences = MMKV.mmkvWithID("myData");
// 迁移旧数据
{
SharedPreferences old_man = getSharedPreferences("myData", MODE_PRIVATE);
preferences.importFromSharedPreferences(old_man);
old_man.edit().clear().commit();
}
// 跟以前用法一样
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean("bool", true);
editor.putInt("int", Integer.MIN_VALUE);
editor.putLong("long", Long.MAX_VALUE);
editor.putFloat("float", -3.14f);
editor.putString("string", "hello, imported");
HashSet set = new HashSet();
set.add("W"); set.add("e"); set.add("C"); set.add("h"); set.add("a"); set.add("t");
editor.putStringSet("string-set", set);
// 无需调用 commit()
//editor.commit();
}
本文基于MMKV1.2.2版本进行解析
当我们在使用MMKV之前,需要在Application中进行初始化,初始化方法上面有讲过,就是调用MMKV的initialize方法,代码如下所示:
public static String initialize(Context context) {
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
// 日志级别
MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
return initialize(root, (MMKV.LibLoader)null, logLevel);
}
它使用的是内部存储空间下的mmkv文件夹作为根目录,然后调用 initialize 方法,代码如下:
public static String initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel) {
if (loader != null) {
if ("StaticCpp".equals("SharedCpp")) {
loader.loadLibrary("c++_shared");
}
loader.loadLibrary("mmkv");
} else {
if ("StaticCpp".equals("SharedCpp")) {
System.loadLibrary("c++_shared");
}
System.loadLibrary("mmkv");
}
MMKV.rootDir = rootDir;
jniInitialize(MMKV.rootDir, logLevel2Int(logLevel)); // ... 1
return rootDir;
}
在注释1处调用 jniInitialize 这个native 方法进行 Native 层的初始化,代码如下所示:
MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir, jint logLevel) {
if (!rootDir) {
return;
}
const char *kstr = env->GetStringUTFChars(rootDir, nullptr);
if (kstr) {
MMKV::initializeMMKV(kstr, (MMKVLogLevel) logLevel); // ... 1
env->ReleaseStringUTFChars(rootDir, kstr);
}
}
在注释1处调用 MMKV::initializeMMKV
对 MMKV 类进行了初始化,代码如下所示:
void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
g_currentLogLevel = logLevel;
ThreadLock::ThreadOnce(&once_control, initialize);
g_rootDir = rootDir;
mkPath(g_rootDir); // ... 1
MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());
}
在注释1处通过mkPath函数创建对应的根目录。完成Native层的初始化工作。
通过 mmkvWithID
方法可以获取 MMKV 对象,它传入的 mmapID
就对应了 SharedPreferences
中的 name,代表了一个文件对应的 name,而 rootPath
则对应了一个相对根目录的相对路径。
@Nullable
public static MMKV mmkvWithID(String mmapID, String rootPath) {
if (rootDir == null) {
throw new IllegalStateException("You should Call MMKV.initialize() first.");
}
long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, rootPath);
return checkProcessMode(handle, mmapID, SINGLE_PROCESS_MODE);
}
private static MMKV checkProcessMode(long handle, String mmapID, int mode) {
if (handle == 0) {
return null;
}
if (!checkedHandleSet.contains(handle)) {
if (!checkProcessMode(handle)) {
String message;
if (mode == SINGLE_PROCESS_MODE) {
message = "Opening a multi-process MMKV instance [" + mmapID + "] with SINGLE_PROCESS_MODE!";
} else {
message = "Opening a single-process MMKV instance [" + mmapID + "] with MULTI_PROCESS_MODE!";
}
throw new IllegalArgumentException(message);
}
checkedHandleSet.add(handle);
}
return new MMKV(handle);
}
它调用到了 getMMKVWithId
这个 Native 方法,并获取到了一个 handle 变量, 然后通过 handle 构造了 Java 层的 MMKV 对象返回。这是一种很常见的手法,Java 层通过持有 Native 层对象的地址从而与 Native 对象通信(例如 Android 中的 Surface 就采用了这种方式)。getMMKVWithId
对应的Native方法代码如下所示:
MMKV_JNI jlong getMMKVWithID(JNIEnv *env, jobject, jstring mmapID, jint mode, jstring cryptKey, jstring rootPath) {
MMKV *kv = nullptr;
// mmapID 为 null 返回空指针
if (!mmapID) {
return (jlong) kv;
}
string str = jstring2string(env, mmapID);
bool done = false;
// 如果cryptKey不为null,则需要进行加密
if (cryptKey) {
// 获取加密的key,最后调用 MMKV::mmkvWithID
string crypt = jstring2string(env, cryptKey);
if (crypt.length() > 0) {
if (rootPath) {
string path = jstring2string(env, rootPath);
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不传入加密可以,表示不进行加密。
if (!done) {
if (rootPath) {
string path = jstring2string(env, rootPath);
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::mmkvWithID
函数代码如下所示:
MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath) {
if (mmapID.empty()) {
return nullptr;
}
// 加锁
SCOPED_LOCK(g_instanceLock);
// 将mmapID 与 rootPath 结合生成 mmapKey
auto mmapKey = mmapedKVKey(mmapID, rootPath);
// 通过 mmapKey 在 map 中查找对应的 MMKV 对象并返回
auto itr = g_instanceDic->find(mmapKey);
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second;
return kv;
}
// 如果不存在,则创建路径并构建MMKV对象并加入到 map 中。
if (rootPath) {
if (!isFileExist(*rootPath)) {
if (!mkPath(*rootPath)) {
return nullptr;
}
}
MMKVInfo("prepare to load %s (id %s) from rootPath %s", mmapID.c_str(), mmapKey.c_str(), rootPath->c_str());
}
auto kv = new MMKV(mmapID, size, mode, cryptKey, rootPath);
(*g_instanceDic)[mmapKey] = kv;
return kv;
}
此函数的步骤如下所示:
1)通过 mmapedKVKey
方法对 mmapID
及 relativePath
进行结合生成了对应的 mmapKey
,它会将它们两者的结合经过 md5 从而生成对应的 key,主要目的是为了支持不同相对路径下的同名 mmapID
。
2)通过 mmapKey
在 g_instanceDic
这个 map 中查找对应的 MMKV 对象,如果找到直接返回。
3)如果找不到对应的 MMKV 对象,构建一个新的 MMKV 对象,加入 map 后返回。
接下来我们分析 MMKV 的构造函数中做了什么,代码如下所示:
MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath)
: m_mmapID(mmapedKVKey(mmapID, rootPath)) // historically Android mistakenly use mmapKey as mmapID
, m_path(mappedKVPathWithID(m_mmapID, mode, rootPath))
, m_crcPath(crcPathWithID(m_mmapID, mode, rootPath))
, m_dic(nullptr)
, m_dicCrypt(nullptr)
, m_file(new MemoryFile(m_path, size, (mode & MMKV_ASHMEM) ? MMFILE_TYPE_ASHMEM : MMFILE_TYPE_FILE))
, m_metaFile(new MemoryFile(m_crcPath, DEFAULT_MMAP_SIZE, m_file->m_fileType))
, m_metaInfo(new MMKVMetaInfo())
, m_crypter(nullptr)
, m_lock(new ThreadLock())
, m_fileLock(new FileLock(m_metaFile->getFd(), (mode & MMKV_ASHMEM)))
, m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
, m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
, m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0 || (mode & CONTEXT_MODE_MULTI_PROCESS) != 0) {
m_actualSize = 0;
m_output = nullptr;
m_fileModeLock = nullptr;
m_sharedProcessModeLock = nullptr;
m_exclusiveProcessModeLock = nullptr;
# ifndef MMKV_DISABLE_CRYPT
// 通过加密 key 构建 AES 加密对象 AESCrypt
if (cryptKey && cryptKey->length() > 0) {
m_dicCrypt = new MMKVMapCrypt();
m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
} else
# endif
{
m_dic = new MMKVMap();
}
m_needLoadFromFile = true;
m_hasFullWriteback = false;
m_crcDigest = 0;
m_sharedProcessLock->m_enable = m_isInterProcess;
m_exclusiveProcessLock->m_enable = m_isInterProcess;
// sensitive zone
// 赋值操作
// 加锁后调用 loadFromFile 加载数据
{
SCOPED_LOCK(m_sharedProcessLock);
loadFromFile();
}
}
这里进行了一些赋值操作,之后如果需要加密则根据用于加密的 cryptKey
生成对应的 AESCrypt
对象用于 AES 加密。最后,加锁后通过 loadFromFile
方法从文件中读取数据,这里的锁是一个跨进程的文件共享锁。接下来查看 loadFromFile函数,代码如下所示:
void MMKV::loadFromFile() {
if (m_metaFile->isFileValid()) {
m_metaInfo->read(m_metaFile->getMemory());
}
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
if (m_metaInfo->m_version >= MMKVVersionRandomIV) {
m_crypter->resetIV(m_metaInfo->m_vector, sizeof(m_metaInfo->m_vector));
}
}
#endif
if (!m_file->isFileValid()) {
m_file->reloadFromFile(); // ... 1
}
if (!m_file->isFileValid()) {
MMKVError("file [%s] not valid", m_path.c_str());
} else {
// error checking
bool loadFromFile = false, needFullWriteback = false;
// 对文件进行 CRC 校验,如果失败根据策略进行不同对处理
checkDataValid(loadFromFile, needFullWriteback);
MMKVInfo("loading [%s] with %zu actual size, file size %zu, InterProcess %d, meta info "
"version:%u",
m_mmapID.c_str(), m_actualSize, m_file->getFileSize(), m_isInterProcess, m_metaInfo->m_version);
auto ptr = (uint8_t *) m_file->getMemory();
// loading
// // 从文件中读取内容
if (loadFromFile && m_actualSize > 0) {
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 对象,读取文件中的数据。
MMBuffer inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
if (m_crypter) {
clearDictionary(m_dicCrypt);
} else {
clearDictionary(m_dic);
}
if (needFullWriteback) {
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
MiniPBCoder::greedyDecodeMap(*m_dicCrypt, inputBuffer, m_crypter);
} else
#endif
{
MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer);
}
} else {
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
MiniPBCoder::decodeMap(*m_dicCrypt, inputBuffer, m_crypter);
} else
#endif
{
MiniPBCoder::decodeMap(*m_dic, inputBuffer);
}
}
// 构造用于输出的 CodeOutputData
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
m_output->seek(m_actualSize);
// 是否需要回写,将map中的数据写入到文件中。
if (needFullWriteback) {
fullWriteback();
}
} else {
// file not valid or empty, discard everything
SCOPED_LOCK(m_exclusiveProcessLock);
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
if (m_actualSize > 0) {
writeActualSize(0, 0, nullptr, IncreaseSequence);
sync(MMKV_SYNC);
} else {
writeActualSize(0, 0, nullptr, KeepSequence);
}
}
auto count = m_crypter ? m_dicCrypt->size() : m_dic->size();
MMKVInfo("loaded [%s] with %zu key-values", m_mmapID.c_str(), count);
}
m_needLoadFromFile = false;
}
我们先分析注释1处,如果文件不是有效的的,则需要调用 reloadFromFile 函数重新加载。代码如下所示:
void MemoryFile::reloadFromFile() {
# ifdef MMKV_ANDROID
if (m_fileType == MMFILE_TYPE_ASHMEM) {
return;
}
# endif
if (isFileValid()) {
MMKVWarning("calling reloadFromFile while the cache [%s] is still valid", m_name.c_str());
MMKV_ASSERT(0);
clearMemoryCache();
}
// 打开对应的文件
m_fd = open(m_name.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, S_IRWXU);
if (m_fd < 0) {
MMKVError("fail to open:%s, %s", m_name.c_str(), strerror(errno));
} else {
FileLock fileLock(m_fd);
InterProcessLock lock(&fileLock, ExclusiveLockType);
SCOPED_LOCK(&lock);
mmkv::getFileSize(m_fd, m_size);
// round up to (n * pagesize)
// 将文件大小对齐到页大小的整数倍,用 0 填充不足的部分
if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
size_t roundSize = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
truncate(roundSize);
} else {
auto ret = mmap();
if (!ret) {
doCleanMemoryCache(true);
}
}
# ifdef MMKV_IOS
tryResetFileProtection(m_name);
# endif
}
}
在 reloadFromFile 函数中 首先打开对应的文件,然后将文件大小对齐到页大小的整数倍,用 0 填充不足的部分,具体实现在 truncate 函数中完成,然后在调用 mmap 函数将文件映射到内存。
loadFromFile 函数的主要逻辑如下:
1)打开文件并获取文件大小,将文件的大小对齐到页的整数倍,不足则补 0(与内存映射的原理有关,内存映射是基于页的换入换出机制实现的)。
2)通过 mmap
函数将文件映射到内存中,然后通过 m_file->getMemory() 得到指向该区域的指针 ptr
。
3)对文件进行长度校验及 CRC 校验(循环冗余校验,可以校验文件完整性),在失败的情况下会根据当前策略进行抉择,如果策略是失败时恢复,则继续读取,并且在最后将 map 中的内容回写到文件。
4)通过 ptr
构造出一块用于管理 MMKV 映射内存的 MMBuffer
对象,如果需要解密,通过之前构造的 AESCrypt
进行解密。
5)由于 MMKV 使用了 protobuf 进行序列化,通过 MiniPBCoder::decodeMap
方法将 protobuf 转换成对应的 map。
6)构造用于输出的 CodedOutputData
类,如果需要回写(CRC 校验或文件长度校验失败),则调用 fullWriteback
方法将 map 中的数据回写到文件。
Java 层的 MMKV 对象继承了 SharedPreferences
及 SharedPreferences.Editor
接口并实现了一系列如 putInt
、putLong
的方法用于对存储的数据进行修改,我们以 putInt
为例:
@Override
public Editor putInt(String key, int value) {
encodeInt(nativeHandle, key, value);
return this;
}
它调用到了 encodeInt
这个 Native 方法:
MMKV_JNI jboolean encodeInt(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint value) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((int32_t) value, key);
}
return (jboolean) false;
}
这里将 Java 层持有的 NativeHandle 转为了对应的 MMKV 对象,之后调用了其 set
函数:
bool MMKV::set(int32_t value, MMKVKey_t key) {
if (isKeyEmpty(key)) {
return false;
}
size_t size = pbInt32Size(value);
// 构造值对应的MMBuffer ,通过 CodeOutputData 将其写入 Buffer
MMBuffer data(size);
CodedOutputData output(data.getPtr(), size);
output.writeInt32(value);
return setDataForKey(move(data), key);
}
set 函数首先获取到了写入的 value 在 protobuf 中所占据的大小,之后为其构造了对应的 MMBuffer
并将数据写入了这段 Buffer,最后调用到了 setDataForKey
函数,同时可以发现 CodedOutputData
是与 Buffer 交互的桥梁,可以通过它实现向 MMBuffer
中写入数据。
通过 Java 层 MMKV 的 remove 方法可以实现删除操作:
@Override
public Editor remove(String key) {
removeValueForKey(key);
return this;
}
它调用了 removeValueForKey
这个 Native 方法:
MMKV_JNI void removeValueForKey(JNIEnv *env, jobject instance, jlong handle, jstring oKey) {
// 通过java层的handle获取Native层的 MMKV对象指针。
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
kv->removeValueForKey(key);
}
}
调用了 Native 层 MMKV 的 removeValueForKey
函数:
void MMKV::removeValueForKey(MMKVKey_t key) {
if (isKeyEmpty(key)) {
return;
}
// 获取锁
SCOPED_LOCK(m_lock);
SCOPED_LOCK(m_exclusiveProcessLock);
// 检查数据是否已经加载到了内存
checkLoadData();
removeDataForKey(key);
}
调用了 removeDataForKey
方法:
bool MMKV::removeDataForKey(MMKVKey_t key) {
if (isKeyEmpty(key)) {
return false;
}
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
auto itr = m_dicCrypt->find(key);
if (itr != m_dicCrypt->end()) {
m_hasFullWriteback = false;
static MMBuffer nan;
# ifdef MMKV_APPLE
auto ret = appendDataWithKey(nan, key, itr->second);
if (ret.first) {
auto oldKey = itr->first;
m_dicCrypt->erase(itr);
[oldKey release];
}
# else
auto ret = appendDataWithKey(nan, key);
if (ret.first) {
m_dicCrypt->erase(itr);
}
# endif
return ret.first;
}
} else
#endif // MMKV_DISABLE_CRYPT
{
auto itr = m_dic->find(key);
if (itr != m_dic->end()) {
m_hasFullWriteback = false;
static MMBuffer nan;
auto ret = appendDataWithKey(nan, itr->second);
if (ret.first) {
#ifdef MMKV_APPLE
auto oldKey = itr->first;
m_dic->erase(itr);
[oldKey release];
#else
m_dic->erase(itr);
#endif
}
return ret.first;
}
}
return false;
}
这里实际上是构造了一条 size 为 0 的 MMBuffer
并调用 appendDataWithKey
将其 append 到 protobuf 文件中,并将 key 对应的内容从 map 中删除。读取时发现它的 size 为 0,则会认为这条数据已经删除。
我们通过 getInt
、getLong
等操作可以实现对数据的读取,我们以 getInt
为例:
@Override
public int getInt(String key, int defValue) {
return decodeInt(nativeHandle, key, defValue);
}
它调用到了 decodeInt
这个 Native 方法:
MMKV_JNI jint decodeInt(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint defaultValue) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jint) kv->getInt32(key, defaultValue);
}
return defaultValue;
}
它调用到了 MMKV.getInt32ForKey
方法:
int32_t MMKV::getInt32(MMKVKey_t key, int32_t defaultValue) {
if (isKeyEmpty(key)) {
return defaultValue;
}
SCOPED_LOCK(m_lock);
auto data = getDataForKey(key);
if (data.length() > 0) {
try {
CodedInputData input(data.getPtr(), data.length());
return input.readInt32();
} catch (std::exception &exception) {
MMKVError("%s", exception.what());
}
}
return defaultValue;
}
它首先调用了 getDataForKey
方法获取到了 key 对应的 MMBuffer
,之后通过 CodedInputData
将数据读出并返回。可以发现,长度为 0 时会将其视为不存在,返回默认值。
MMBuffer MMKV::getDataForKey(MMKVKey_t key) {
checkLoadData();
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
auto itr = m_dicCrypt->find(key);
if (itr != m_dicCrypt->end()) {
auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
return itr->second.toMMBuffer(basePtr, m_crypter);
}
} else
#endif
{
auto itr = m_dic->find(key);
if (itr != m_dic->end()) {
auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
return itr->second.toMMBuffer(basePtr);
}
}
MMBuffer nan;
return nan;
}
这里实际上是通过在 Map
中寻找从而实现,找不到会返回 size 为 0 的 Buffer。
MMKV 中,在一些特定的情景下,会通过 fullWriteback
函数立即将 map 的内容回写到文件。
回写时机主要有以下几个:
1)通过 MMKV.reKey
方法修改加密的 key。
2)删除一系列的 key 时(通过 removeValuesForKeys
方法)
3)读取文件时文件校验或 CRC 校验失败。
bool MMKV::fullWriteback(AESCrypt *newCrypter) {
if (m_hasFullWriteback) {
return true;
}
if (m_needLoadFromFile) {
return true;
}
if (!isFileValid()) {
MMKVWarning("[%s] file not valid", m_mmapID.c_str());
return false;
}
// 如果 map 空了,直接清空文件
if (m_crypter ? m_dicCrypt->empty() : m_dic->empty()) {
clearAll();
return true;
}
auto preparedData = m_crypter ? prepareEncode(*m_dicCrypt) : prepareEncode(*m_dic);
auto sizeOfDic = preparedData.second;
SCOPED_LOCK(m_exclusiveProcessLock);
if (sizeOfDic > 0) {
auto fileSize = m_file->getFileSize();
if (sizeOfDic + Fixed32Size <= fileSize) {
// 如果空间够写,直接写入
return doFullWriteBack(move(preparedData), newCrypter);
} else {
assert(0);
assert(newCrypter == nullptr);
// ensureMemorySize will extend file & full rewrite, no need to write back again
// 空间不够写入,调用 ensureMemorySize 进行扩容
return ensureMemorySize(sizeOfDic + Fixed32Size - fileSize);
}
}
return false;
}
这里首先在 map 为空的情况下,由于代表了所有数据已被删除,因此通过 clearAll
清除了文件与数据。否则它会对当前映射空间是否足够写入 map 中回写的数据,如果足够则会将数据写入,否则会调用 ensureMemorySize
从而进行内存重整与扩容。