深入剖析:Android MMKV 数据持久化全流程揭秘
一、引言
在 Android 开发的世界里,数据持久化是一个核心且关键的功能。它能够让应用在设备重启或者应用关闭后依然保留重要的数据,保证用户体验的连贯性。而 MMKV(Multi - Process Key - Value)作为腾讯开源的一款高性能键值对存储框架,凭借其高效、稳定以及多进程支持等显著优势,在众多 Android 开发者中广泛应用。本文将对 Android MMKV 数据持久化的具体过程展开全方位、深入的源码级分析,旨在帮助开发者透彻理解其内部运行机制,从而更灵活、高效地运用该框架。
二、MMKV 概述
2.1 MMKV 简介
MMKV 是基于 mmap 内存映射技术和 Protobuf 数据编码实现的高性能键值对存储框架,它专为 Android 和 iOS 平台量身打造。相较于 Android 传统的 SharedPreferences,MMKV 在性能、多进程支持等方面都有显著提升。
2.2 MMKV 优势
- 高性能:通过 mmap 内存映射技术,避免了频繁的 I/O 操作,极大地提高了数据的读写速度。
- 多进程支持:可以在多个进程之间安全、高效地共享数据,解决了
SharedPreferences在多进程环境下的诸多问题。 - 简单易用:提供了与
SharedPreferences类似的 API,方便开发者快速上手和集成。
2.3 MMKV 应用场景
MMKV 适用于各种需要高效数据持久化的场景,比如:
- 用户配置信息存储:像用户的偏好设置、主题选择等。
- 缓存数据存储:用于缓存网络请求结果、图片等临时数据,提升应用的响应速度。
- 多进程数据共享:在多进程应用中,实现不同进程间的数据同步和共享。
三、MMKV 初始化流程
3.1 初始化方法调用
在 Android 应用中使用 MMKV 之前,需要先进行初始化操作。通常在 Application 类的 onCreate 方法中调用 MMKV.initialize 方法。以下是示例代码:
import android.app.Application;
import com.tencent.mmkv.MMKV;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 调用 MMKV 的初始化方法,传入应用的上下文
String rootDir = MMKV.initialize(this);
}
}
3.2 源码分析
MMKV.initialize 方法的源码如下:
// MMKV.java 文件中的 initialize 方法
public static String initialize(Context context) {
// 获取应用的文件目录
File root = context.getFilesDir();
// 拼接出 MMKV 的存储根目录路径
String rootDir = root.getAbsolutePath() + "/mmkv";
// 调用 native 方法进行初始化,传入存储根目录路径
nativeInitialize(rootDir);
return rootDir;
}
在上述代码中,首先获取应用的文件目录,然后在该目录下创建一个名为 mmkv 的子目录作为 MMKV 的存储根目录。最后调用 nativeInitialize 这个 native 方法进行进一步的初始化操作。
nativeInitialize 方法在 JNI 层的实现如下:
// jni 层的初始化方法
extern "C" JNIEXPORT void JNICALL
Java_com_tencent_mmkv_MMKV_nativeInitialize(JNIEnv *env, jclass clazz, jstring rootDir) {
// 将 Java 字符串转换为 C++ 字符串
const char *cRootDir = env->GetStringUTFChars(rootDir, nullptr);
if (cRootDir) {
// 调用 MMKV 的初始化方法,传入存储根目录路径
MMKV::initializeMMKV(cRootDir);
// 释放 Java 字符串占用的资源
env->ReleaseStringUTFChars(rootDir, cRootDir);
}
}
在 JNI 层,将 Java 传入的存储根目录路径转换为 C++ 字符串,然后调用 MMKV::initializeMMKV 方法进行初始化。
MMKV::initializeMMKV 方法的实现如下:
// MMKV.cpp 文件中的 initializeMMKV 方法
void MMKV::initializeMMKV(const char *rootDir) {
// 检查根目录是否为空
if (!rootDir || strlen(rootDir) == 0) {
return;
}
// 保存根目录路径
g_rootDir = strdup(rootDir);
// 创建根目录
mkdir(g_rootDir, 0777);
// 初始化锁
g_instanceLock = new pthread_rwlock_t;
pthread_rwlock_init(g_instanceLock, nullptr);
// 初始化文件锁
g_fileLock = new pthread_rwlock_t;
pthread_rwlock_init(g_fileLock, nullptr);
}
在这个方法中,首先检查根目录是否为空,若不为空则保存根目录路径,并创建该目录。接着初始化读写锁,用于后续对实例和文件的并发访问控制。
3.3 初始化流程总结
MMKV 的初始化流程主要包括以下几个步骤:
- 在 Java 层获取应用的文件目录,并创建 MMKV 的存储根目录。
- 调用 JNI 层的
nativeInitialize方法,将存储根目录路径传递给 C++ 代码。 - 在 C++ 层,保存根目录路径,创建根目录,并初始化读写锁。
四、MMKV 实例创建
4.1 获取 MMKV 实例
在初始化完成后,可以通过 MMKV.defaultMMKV 方法获取默认的 MMKV 实例,或者通过 MMKV.mmkvWithID 方法获取指定 ID 的 MMKV 实例。以下是示例代码:
import com.tencent.mmkv.MMKV;
public class MainActivity {
public void createMMKVInstance() {
// 获取默认的 MMKV 实例
MMKV defaultMMKV = MMKV.defaultMMKV();
// 获取指定 ID 的 MMKV 实例
MMKV customMMKV = MMKV.mmkvWithID("custom_id");
}
}
4.2 源码分析
MMKV.defaultMMKV 方法的源码如下:
// MMKV.java 文件中的 defaultMMKV 方法
public static MMKV defaultMMKV() {
// 调用 native 方法获取默认的 MMKV 实例
long handle = nativeDefaultMMKV();
// 根据获取到的句柄创建 MMKV 实例
return new MMKV(handle);
}
在这个方法中,调用 nativeDefaultMMKV 这个 native 方法获取默认的 MMKV 实例的句柄,然后根据句柄创建 Java 层的 MMKV 实例。
nativeDefaultMMKV 方法在 JNI 层的实现如下:
// jni 层的获取默认 MMKV 实例的方法
extern "C" JNIEXPORT jlong JNICALL
Java_com_tencent_mmkv_MMKV_nativeDefaultMMKV(JNIEnv *env, jclass clazz) {
// 调用 MMKV 的获取默认实例方法
MMKV *kv = MMKV::defaultMMKV();
// 返回 MMKV 实例的指针作为句柄
return reinterpret_cast<jlong>(kv);
}
在 JNI 层,调用 MMKV::defaultMMKV 方法获取默认的 MMKV 实例,并将其指针作为句柄返回给 Java 层。
MMKV::defaultMMKV 方法的实现如下:
// MMKV.cpp 文件中的 defaultMMKV 方法
MMKV *MMKV::defaultMMKV() {
// 加读锁,保证并发安全
pthread_rwlock_rdlock(g_instanceLock);
// 从全局实例映射中查找默认实例
auto itr = g_instanceDic.find(DEFAULT_MMAP_ID);
if (itr != g_instanceDic.end()) {
// 如果找到,获取实例
MMKV *kv = itr->second;
// 解锁
pthread_rwlock_unlock(g_instanceLock);
return kv;
}
// 解锁
pthread_rwlock_unlock(g_instanceLock);
// 加写锁
pthread_rwlock_wrlock(g_instanceLock);
// 再次从全局实例映射中查找默认实例
itr = g_instanceDic.find(DEFAULT_MMAP_ID);
if (itr != g_instanceDic.end()) {
// 如果找到,获取实例
MMKV *kv = itr->second;
// 解锁
pthread_rwlock_unlock(g_instanceLock);
return kv;
}
// 创建默认的 MMKV 实例
MMKV *kv = new MMKV(DEFAULT_MMAP_ID);
// 将实例添加到全局实例映射中
g_instanceDic[DEFAULT_MMAP_ID] = kv;
// 解锁
pthread_rwlock_unlock(g_instanceLock);
return kv;
}
在这个方法中,首先加读锁从全局实例映射中查找默认的 MMKV 实例。如果找到则返回该实例;如果未找到,释放读锁并加写锁,再次查找。若还是未找到,则创建默认的 MMKV 实例,并将其添加到全局实例映射中,最后返回该实例。
MMKV.mmkvWithID 方法的源码和 defaultMMKV 方法类似,只是在获取实例时使用的 ID 不同。
4.3 实例创建流程总结
MMKV 实例的创建流程主要包括以下几个步骤:
- 在 Java 层调用
defaultMMKV或mmkvWithID方法。 - 调用 JNI 层的相应 native 方法,将请求传递给 C++ 代码。
- 在 C++ 层,先尝试从全局实例映射中查找指定 ID 的实例。若找到则返回;若未找到,则创建新的实例并添加到全局实例映射中。
五、MMKV 数据写入过程
5.1 数据写入方法调用
MMKV 提供了多种数据写入方法,如 encode 方法可以写入不同类型的数据。以下是示例代码:
import com.tencent.mmkv.MMKV;
public class WriteDataExample {
public void writeData() {
// 获取默认的 MMKV 实例
MMKV mmkv = MMKV.defaultMMKV();
// 写入字符串数据
mmkv.encode("key_string", "value_string");
// 写入整数数据
mmkv.encode("key_int", 123);
// 写入布尔数据
mmkv.encode("key_bool", true);
}
}
5.2 源码分析
MMKV.encode 方法的源码如下:
// MMKV.java 文件中的 encode 方法(以写入字符串为例)
public boolean encode(String key, String value) {
if (key == null || value == null) {
return false;
}
// 调用 native 方法进行字符串数据的写入
return nativeEncodeString(mHandle, key, value);
}
在这个方法中,首先检查键和值是否为空,若不为空则调用 nativeEncodeString 这个 native 方法进行字符串数据的写入。
nativeEncodeString 方法在 JNI 层的实现如下:
// jni 层的写入字符串数据的方法
extern "C" JNIEXPORT jboolean JNICALL
Java_com_tencent_mmkv_MMKV_nativeEncodeString(JNIEnv *env, jobject thiz, jlong handle, jstring key, jstring value) {
// 获取 MMKV 实例
MMKV *kv = reinterpret_cast<MMKV*>(handle);
if (!kv) {
return JNI_FALSE;
}
// 将 Java 字符串转换为 C++ 字符串
const char *cKey = env->GetStringUTFChars(key, nullptr);
const char *cValue = env->GetStringUTFChars(value, nullptr);
if (cKey && cValue) {
// 调用 MMKV 的写入字符串方法
bool result = kv->setString(cKey, cValue);
// 释放 Java 字符串占用的资源
env->ReleaseStringUTFChars(key, cKey);
env->ReleaseStringUTFChars(value, cValue);
return result ? JNI_TRUE : JNI_FALSE;
}
if (cKey) {
env->ReleaseStringUTFChars(key, cKey);
}
if (cValue) {
env->ReleaseStringUTFChars(value, cValue);
}
return JNI_FALSE;
}
在 JNI 层,首先根据句柄获取 MMKV 实例,然后将 Java 字符串转换为 C++ 字符串,调用 MMKV 实例的 setString 方法进行字符串数据的写入,最后释放 Java 字符串占用的资源。
MMKV::setString 方法的实现如下:
// MMKV.cpp 文件中的 setString 方法
bool MMKV::setString(const char *key, const char *value) {
// 加写锁,保证并发安全
pthread_rwlock_wrlock(&m_lock);
// 查找键对应的记录
auto itr = m_dic.find(key);
if (itr != m_dic.end()) {
// 如果找到记录,删除该记录
delete itr->second;
m_dic.erase(itr);
}
// 创建新的记录
MMKVValue *newValue = new MMKVValue(value);
// 将新记录添加到字典中
m_dic[key] = newValue;
// 标记数据有更新
m_needLoadFromFile = false;
m_hasFullWriteBack = false;
// 进行数据持久化操作
bool result = appendDataWithKey(key, newValue);
// 解锁
pthread_rwlock_unlock(&m_lock);
return result;
}
在这个方法中,首先加写锁,查找键对应的记录。如果找到则删除该记录,然后创建新的记录并添加到字典中。标记数据有更新,调用 appendDataWithKey 方法进行数据持久化操作,最后解锁并返回操作结果。
MMKV::appendDataWithKey 方法的实现如下:
// MMKV.cpp 文件中的 appendDataWithKey 方法
bool MMKV::appendDataWithKey(const char *key, MMKVValue *value) {
// 计算需要写入的数据长度
size_t keyLen = strlen(key);
size_t valueLen = value->length();
size_t totalLen = 2 * sizeof(uint32_t) + keyLen + valueLen;
// 检查文件大小是否足够
if (m_actualSize + totalLen > m_size) {
// 如果不够,进行文件扩容
if (!ensureMemorySize(m_actualSize + totalLen)) {
return false;
}
}
// 获取文件指针
char *ptr = m_ptr + m_actualSize;
// 写入键的长度
*((uint32_t *) ptr) = (uint32_t) keyLen;
ptr += sizeof(uint32_t);
// 写入键
memcpy(ptr, key, keyLen);
ptr += keyLen;
// 写入值的长度
*((uint32_t *) ptr) = (uint32_t) valueLen;
ptr += sizeof(uint32_t);
// 写入值
memcpy(ptr, value->data(), valueLen);
// 更新实际使用的文件大小
m_actualSize += totalLen;
// 同步文件内容到磁盘
msync(m_ptr, m_actualSize, MS_SYNC);
return true;
}
在这个方法中,首先计算需要写入的数据长度,检查文件大小是否足够。如果不够则进行文件扩容,然后将键的长度、键、值的长度和值依次写入文件,更新实际使用的文件大小,最后调用 msync 方法将文件内容同步到磁盘。
5.3 数据写入流程总结
MMKV 数据写入的流程主要包括以下几个步骤:
- 在 Java 层调用
encode方法,传入键和值。 - 调用 JNI 层的相应 native 方法,将请求传递给 C++ 代码。
- 在 C++ 层,加写锁,查找键对应的记录。若找到则删除,然后创建新的记录并添加到字典中。
- 检查文件大小是否足够,若不够则进行文件扩容。
- 将键的长度、键、值的长度和值依次写入文件,更新实际使用的文件大小。
- 调用
msync方法将文件内容同步到磁盘,最后解锁并返回操作结果。
六、MMKV 数据读取过程
6.1 数据读取方法调用
MMKV 提供了多种数据读取方法,如 decodeString、decodeInt、decodeBool 等。以下是示例代码:
import com.tencent.mmkv.MMKV;
public class ReadDataExample {
public void readData() {
// 获取默认的 MMKV 实例
MMKV mmkv = MMKV.defaultMMKV();
// 读取字符串数据
String stringValue = mmkv.decodeString("key_string");
// 读取整数数据
int intValue = mmkv.decodeInt("key_int", 0);
// 读取布尔数据
boolean boolValue = mmkv.decodeBool("key_bool", false);
}
}
6.2 源码分析
MMKV.decodeString 方法的源码如下:
// MMKV.java 文件中的 decodeString 方法
public String decodeString(String key) {
if (key == null) {
return null;
}
// 调用 native 方法进行字符串数据的读取
return nativeDecodeString(mHandle, key);
}
在这个方法中,首先检查键是否为空,若不为空则调用 nativeDecodeString 这个 native 方法进行字符串数据的读取。
nativeDecodeString 方法在 JNI 层的实现如下:
// jni 层的读取字符串数据的方法
extern "C" JNIEXPORT jstring JNICALL
Java_com_tencent_mmkv_MMKV_nativeDecodeString(JNIEnv *env, jobject thiz, jlong handle, jstring key) {
// 获取 MMKV 实例
MMKV *kv = reinterpret_cast<MMKV*>(handle);
if (!kv) {
return nullptr;
}
// 将 Java 字符串转换为 C++ 字符串
const char *cKey = env->GetStringUTFChars(key, nullptr);
if (cKey) {
// 调用 MMKV 的读取字符串方法
std::string value = kv->getString(cKey);
// 释放 Java 字符串占用的资源
env->ReleaseStringUTFChars(key, cKey);
if (!value.empty()) {
// 将 C++ 字符串转换为 Java 字符串并返回
return env->NewStringUTF(value.c_str());
}
}
if (cKey) {
env->ReleaseStringUTFChars(key, cKey);
}
return nullptr;
}
在 JNI 层,首先根据句柄获取 MMKV 实例,然后将 Java 字符串转换为 C++ 字符串,调用 MMKV 实例的 getString 方法进行字符串数据的读取,最后将 C++ 字符串转换为 Java 字符串并返回。
MMKV::getString 方法的实现如下:
// MMKV.cpp 文件中的 getString 方法
std::string MMKV::getString(const char *key) {
// 加读锁,保证并发安全
pthread_rwlock_rdlock(&m_lock);
// 检查是否需要从文件加载数据
if (m_needLoadFromFile) {
// 如果需要,加载数据
loadFromFile();
}
// 查找键对应的记录
auto itr = m_dic.find(key);
if (itr != m_dic.end()) {
// 如果找到记录,获取值
MMKVValue *value = itr->second;
if (value->type() == MMKVValue::String) {
// 如果值是字符串类型,返回字符串
std::string result(value->data(), value->length());
// 解锁
pthread_rwlock_unlock(&m_lock);
return result;
}
}
// 解锁
pthread_rwlock_unlock(&m_lock);
return "";
}
在这个方法中,首先加读锁,检查是否需要从文件加载数据。如果需要则调用 loadFromFile 方法加载数据,然后查找键对应的记录。如果找到且值是字符串类型,则返回字符串,最后解锁。
MMKV::loadFromFile 方法的实现如下:
// MMKV.cpp 文件中的 loadFromFile 方法
void MMKV::loadFromFile() {
// 清空字典
for (auto itr = m_dic.begin(); itr != m_dic.end(); ++itr) {
delete itr->second;
}
m_dic.clear();
// 读取文件内容
size_t offset = 0;
while (offset < m_actualSize) {
// 读取键的长度
uint32_t keyLen = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 读取键
std::string key(m_ptr + offset, keyLen);
offset += keyLen;
// 读取值的长度
uint32_t valueLen = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 读取值
MMKVValue *value = new MMKVValue(m_ptr + offset, valueLen);
offset += valueLen;
// 将键值对添加到字典中
m_dic[key] = value;
}
// 标记数据已加载
m_needLoadFromFile = false;
}
在这个方法中,首先清空字典,然后从文件中依次读取键的长度、键、值的长度和值,将键值对添加到字典中,最后标记数据已加载。
6.3 数据读取流程总结
MMKV 数据读取的流程主要包括以下几个步骤:
- 在 Java 层调用
decode方法,传入键。 - 调用 JNI 层的相应 native 方法,将请求传递给 C++ 代码。
- 在 C++ 层,加读锁,检查是否需要从文件加载数据。若需要则调用
loadFromFile方法加载数据。 - 查找键对应的记录,若找到且值类型匹配,则返回值,最后解锁。
七、MMKV 文件管理
7.1 文件创建与打开
在 MMKV 实例创建时,会创建或打开相应的数据文件。MMKV 类的构造函数中涉及文件的创建与打开操作。以下是相关代码:
// MMKV.cpp 文件中的构造函数
MMKV::MMKV(const std::string &mmapID) : m_mmapID(mmapID) {
// 拼接数据文件的路径
std::string path = getMMKVPath(mmapID);
// 打开文件
m_fd = open(path.c_str(), O_RDWR | O_CREAT, 0666);
if (m_fd < 0) {
// 打开文件失败,记录错误信息
perror("open");
} else {
// 获取文件的状态信息
struct stat st;
if (fstat(m_fd, &st) != 0) {
// 获取文件状态失败,记录错误信息
perror("fstat");
} else {
// 获取文件的大小
m_size = st.st_size;
if (m_size == 0) {
// 如果文件大小为 0,调整文件大小为默认值
if (ftruncate(m_fd, DEFAULT_MMAP_SIZE) != 0) {
// 调整文件大小失败,记录错误信息
perror("ftruncate");
}
m_size = DEFAULT_MMAP_SIZE;
}
// 进行内存映射
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
} else {
// 标记需要从文件加载数据
m_needLoadFromFile = true;
}
}
}
// 初始化锁
pthread_rwlock_init(&m_lock, nullptr);
}
在构造函数中,首先拼接数据文件的路径,然后使用 open 函数打开文件。如果文件大小为 0,则使用 ftruncate 函数调整文件大小为默认值。接着使用 mmap 函数进行内存映射,最后初始化锁。
7.2 文件扩容
当文件大小不足以存储新的数据时,MMKV 会进行文件扩容。MMKV::ensureMemorySize 方法实现了文件扩容功能。以下是相关代码:
// MMKV.cpp 文件中的 ensureMemorySize 方法
bool MMKV::ensureMemorySize(size_t newSize) {
// 计算新的文件大小
size_t oldSize = m_size;
while (m_size < newSize) {
// 每次扩容为原来的 2 倍
m_size *= 2;
}
// 解锁
pthread_rwlock_unlock(&m_lock);
// 解除内存映射
if (munmap(m_ptr, oldSize) != 0) {
// 解除内存映射失败,记录错误信息
perror("munmap");
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// 调整文件大小
if (ftruncate(m_fd, m_size) != 0) {
// 调整文件大小失败,记录错误信息
perror("ftruncate");
// 重新进行内存映射
m_ptr = (char *) mmap(nullptr, oldSize, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
}
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// 重新进行内存映射
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return true;
}
在这个方法中,首先计算新的文件大小,每次扩容为原来的 2 倍。然后解除原来的内存映射,调整文件大小,最后重新进行内存映射。
7.3 文件同步
为了保证数据的持久化,MMKV 会在数据写入后进行文件同步。MMKV::appendDataWithKey 方法中调用了 msync 函数进行文件同步。以下是相关代码:
// MMKV.cpp 文件中的 appendDataWithKey 方法(部分代码)
bool MMKV::appendDataWithKey(const char *key, MMKVValue *value) {
// ... 前面的代码省略 ...
// 同步文件内容到磁盘
msync(m_ptr, m_actualSize, MS_SYNC);
return true;
}
msync 函数将内存中的数据同步到磁盘,保证数据的持久化。
7.4 文件管理流程总结
MMKV 文件管理的流程主要包括以下几个步骤:
- 在实例创建时,拼接数据文件的路径,打开文件。若文件大小为 0,则调整文件大小为默认值,并进行内存映射。
- 当文件大小不足以存储新的数据时,进行文件扩容。先计算新的文件大小,解除原来的内存映射,调整文件大小,最后重新进行内存映射。
- 在数据写入后,调用
msync函数将内存中的数据同步到磁盘,保证数据的持久化。
八、MMKV 多进程支持
8.1 多进程数据同步原理
MMKV 通过文件锁和内存映射的机制实现多进程数据同步。多个进程可以同时对同一个数据文件进行内存映射,通过文件锁来保证对文件的并发访问安全。当一个进程对文件进行写入操作时,会先获取文件锁,写入完成后释放文件锁,其他进程在获取到文件锁后才能进行读写操作。
8.2 多进程模式下的实例创建
在多进程模式下,可以通过 MMKV.mmkvWithID 方法并指定 MMKV.MULTI_PROCESS_MODE 来创建支持多进程的 MMKV 实例。以下是示例代码:
import com.tencent.mmkv.MMKV;
public class MultiProcessExample {
public void createMultiProcessInstance() {
// 创建支持多进程的 MMKV 实例
MMKV multiProcessMMKV = MMKV.mmkvWithID("multi_process_id", MMKV.MULTI_PROCESS_MODE);
}
}
8.3 多进程数据读写
在多进程模式下,不同进程对同一个 MMKV 实例进行数据读写操作时,会自动进行数据同步。例如,一个进程写入数据后,另一个进程可以立即读取到最新的数据。以下是示例代码:
import com.tencent.mmkv.MMKV;
// 进程 1 写入数据
public class Process1 {
public void writeData() {
MMKV multiProcessMMKV = MMKV.mmkvWithID("multi_process_id", MMKV.MULTI_PROCESS_MODE);
multiProcessMMKV.encode("key", "value");
}
}
// 进程 2 读取数据
public class Process2 {
public void readData() {
MMKV multiProcessMMKV = MMKV.mmkvWithID("multi_process_id", MMKV.MULTI_PROCESS_MODE);
String value = multiProcessMMKV.decodeString("key");
}
}
8.4 多进程支持源码分析
在 C++ 层,MMKV 类通过文件锁来实现多进程数据同步。以下是相关代码:
// MMKV.cpp 文件中的部分代码
// 初始化文件锁
pthread_rwlock_t *g_fileLock;
// 在构造函数中初始化文件锁
MMKV::MMKV(const std::string &mmapID) : m_mmapID(mmapID) {
// ... 前面的代码省略 ...
// 初始化锁
pthread_rwlock_init(&m_lock, nullptr);
// 初始化文件锁
if (m_isMultiProcess) {
pthread_rwlock_init(g_fileLock, nullptr);
}
}
// 在写入数据时加文件锁
bool MMKV::setString(const char *key, const char *value) {
if (m_isMultiProcess) {
// 加文件写锁
pthread_rwlock_wrlock(g_fileLock);
}
// 加实例写锁
pthread_rwlock_wrlock(&m_lock);
// ... 写入数据的代码省略 ...
// 解锁实例写锁
pthread_rwlock_unlock(&m_lock);
if (m_isMultiProcess) {
// 解锁文件写锁
pthread_rwlock_unlock(g_fileLock);
}
return result;
}
// 在读取数据时加文件锁
std::string MMKV::getString(const char *key) {
if (m_isMultiProcess) {
// 加文件读锁
pthread_rwlock_rdlock(g_fileLock);
}
// 加实例读锁
pthread_rwlock_rdlock(&m_lock);
// ... 读取数据的代码省略 ...
// 解锁实例读锁
pthread_rwlock_unlock(&m_lock);
if (m_isMultiProcess) {
// 解锁文件读锁
pthread_rwlock_unlock(g_fileLock);
}
return result;
}
在上述代码中,通过 g_fileLock 来实现文件锁的功能。在多进程模式下,写入数据时加文件写锁,读取数据时加文件读锁,保证多进程数据的并发访问安全。
8.5 多进程支持流程总结
MMKV 多进程支持的流程主要包括以下几个步骤:
- 在创建 MMKV 实例时,指定
MMKV.MULTI_PROCESS_MODE开启多进程模式。 - 在 C++ 层,通过文件锁(
g_fileLock)来保证多进程对文件的并发访问安全。写入数据时加文件写锁,读取数据时加文件读锁。 - 不同进程对同一个 MMKV 实例进行数据读写操作时,会自动进行数据同步,保证数据的一致性。
九、MMKV 数据删除过程
9.1 数据删除方法调用
MMKV 提供了 removeValueForKey 方法用于删除指定键的数据。以下是示例代码:
import com.tencent.mmkv.MMKV;
public class DeleteDataExample {
public void deleteData() {
// 获取默认的 MMKV 实例
MMKV mmkv = MMKV.defaultMMKV();
// 删除指定键的数据
mmkv.removeValueForKey("key");
}
}
9.2 源码分析
MMKV.removeValueForKey 方法的源码如下:
// MMKV.java 文件中的 removeValueForKey 方法
public void removeValueForKey(String key) {
if (key == null) {
return;
}
// 调用 native 方法进行数据删除
nativeRemoveValueForKey(mHandle, key);
}
在这个方法中,首先检查键是否为空,若不为空则调用 nativeRemoveValueForKey 这个 native 方法进行数据删除。
nativeRemoveValueForKey 方法在 JNI 层的实现如下:
// jni 层的删除数据方法
extern "C" JNIEXPORT void JNICALL
Java_com_tencent_mmkv_MMKV_nativeRemoveValueForKey(JNIEnv *env, jobject thiz, jlong handle, jstring key) {
// 获取 MMKV 实例
MMKV *kv = reinterpret_cast<MMKV*>(handle);
if (!kv) {
return;
}
接上部分nativeRemoveValueForKey 方法在 JNI 层的实现:
// jni 层的删除数据方法
extern "C" JNIEXPORT void JNICALL
Java_com_tencent_mmkv_MMKV_nativeRemoveValueForKey(JNIEnv *env, jobject thiz, jlong handle, jstring key) {
// 获取 MMKV 实例
MMKV *kv = reinterpret_cast<MMKV*>(handle);
if (!kv) {
return;
}
// 将 Java 字符串转换为 C++ 字符串
const char *cKey = env->GetStringUTFChars(key, nullptr);
if (cKey) {
// 调用 MMKV 的删除方法
kv->removeValueForKey(cKey);
// 释放 Java 字符串占用的资源
env->ReleaseStringUTFChars(key, cKey);
}
}
在 JNI 层,先根据句柄获取 MMKV 实例,将 Java 字符串转换为 C++ 字符串后,调用 MMKV 实例的 removeValueForKey 方法,最后释放 Java 字符串占用的资源 。
MMKV::removeValueForKey 方法的实现如下:
// MMKV.cpp 文件中的 removeValueForKey 方法
void MMKV::removeValueForKey(const char *key) {
// 加写锁,保证并发安全
pthread_rwlock_wrlock(&m_lock);
// 查找键对应的记录
auto itr = m_dic.find(key);
if (itr != m_dic.end()) {
// 如果找到记录,删除该记录
delete itr->second;
m_dic.erase(itr);
// 标记数据有更新
m_needLoadFromFile = false;
m_hasFullWriteBack = false;
// 执行数据重写操作,更新文件内容
rewriteData();
}
// 解锁
pthread_rwlock_unlock(&m_lock);
}
在该方法中,首先加写锁,查找键对应的记录。若找到记录,则删除记录并从字典中移除 ,同时标记数据有更新,接着调用 rewriteData 方法执行数据重写操作来更新文件内容,最后解锁 。
MMKV::rewriteData 方法的实现如下:
// MMKV.cpp 文件中的 rewriteData 方法
void MMKV::rewriteData() {
// 临时文件路径
std::string tempPath = getMMKVPath(m_mmapID) + ".tmp";
// 打开临时文件
int tempFd = open(tempPath.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0666);
if (tempFd < 0) {
// 打开临时文件失败,记录错误信息
perror("open temp file");
return;
}
// 计算新数据总长度
size_t totalLen = 0;
for (auto itr = m_dic.begin(); itr != m_dic.end(); ++itr) {
const char *key = itr->first;
MMKVValue *value = itr->second;
size_t keyLen = strlen(key);
size_t valueLen = value->length();
totalLen += 2 * sizeof(uint32_t) + keyLen + valueLen;
}
// 调整临时文件大小
if (ftruncate(tempFd, totalLen) != 0) {
// 调整文件大小失败,记录错误信息
perror("ftruncate temp file");
close(tempFd);
return;
}
// 内存映射临时文件
char *tempPtr = (char *) mmap(nullptr, totalLen, PROT_READ | PROT_WRITE, MAP_SHARED, tempFd, 0);
if (tempPtr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap temp file");
close(tempFd);
return;
}
// 写入数据到临时文件
char *writePtr = tempPtr;
for (auto itr = m_dic.begin(); itr != m_dic.end(); ++itr) {
const char *key = itr->first;
MMKVValue *value = itr->second;
size_t keyLen = strlen(key);
size_t valueLen = value->length();
// 写入键的长度
*((uint32_t *) writePtr) = (uint32_t) keyLen;
writePtr += sizeof(uint32_t);
// 写入键
memcpy(writePtr, key, keyLen);
writePtr += keyLen;
// 写入值的长度
*((uint32_t *) writePtr) = (uint32_t) valueLen;
writePtr += sizeof(uint32_t);
// 写入值
memcpy(writePtr, value->data(), valueLen);
writePtr += valueLen;
}
// 同步临时文件内容到磁盘
msync(tempPtr, totalLen, MS_SYNC);
// 解除临时文件内存映射
if (munmap(tempPtr, totalLen) != 0) {
// 解除内存映射失败,记录错误信息
perror("munmap temp file");
}
// 关闭临时文件
close(tempFd);
// 关闭原文件
close(m_fd);
// 删除原文件
if (unlink(getMMKVPath(m_mmapID).c_str()) != 0) {
// 删除原文件失败,记录错误信息
perror("unlink original file");
}
// 重命名临时文件为原文件名
if (rename(tempPath.c_str(), getMMKVPath(m_mmapID).c_str()) != 0) {
// 重命名失败,记录错误信息
perror("rename temp file");
}
// 重新打开文件
m_fd = open(getMMKVPath(m_mmapID).c_str(), O_RDWR | O_CREAT, 0666);
if (m_fd < 0) {
// 打开文件失败,记录错误信息
perror("open original file after rename");
return;
}
// 获取文件状态信息
struct stat st;
if (fstat(m_fd, &st) != 0) {
// 获取文件状态失败,记录错误信息
perror("fstat original file after rename");
close(m_fd);
return;
}
// 获取文件大小
m_size = st.st_size;
// 重新进行内存映射
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap original file after rename");
close(m_fd);
return;
}
// 更新实际使用的文件大小
m_actualSize = m_size;
}
在 rewriteData 方法中,首先创建临时文件,计算剩余数据的总长度并调整临时文件大小,进行内存映射。然后将剩余的键值对数据依次写入临时文件,同步临时文件内容到磁盘,解除临时文件的内存映射并关闭临时文件 。接着关闭原文件,删除原文件,将临时文件重命名为原文件名,重新打开文件并获取文件大小,进行内存映射,更新实际使用的文件大小 。通过这样的方式,在删除指定键值对后,更新了数据文件的内容,实现了数据的删除持久化 。
9.3 数据删除流程总结
MMKV 数据删除的流程主要包括以下几个步骤:
- 在 Java 层调用
removeValueForKey方法,传入要删除数据的键。 - 调用 JNI 层的
nativeRemoveValueForKey方法,将请求传递给 C++ 代码,在 JNI 层完成字符串类型转换并调用 C++ 层的删除方法 。 - 在 C++ 层,加写锁,查找键对应的记录。若找到记录,则删除记录并从字典中移除,标记数据有更新,调用
rewriteData方法 。 rewriteData方法中,创建临时文件,计算剩余数据总长度并调整临时文件大小,进行内存映射;将剩余键值对数据写入临时文件,同步临时文件内容到磁盘;解除临时文件内存映射,关闭临时文件;关闭、删除原文件,将临时文件重命名为原文件名;重新打开文件,获取文件大小并进行内存映射,更新实际使用的文件大小 。- 最后解锁,完成数据删除操作,确保数据在文件中被正确移除,实现数据持久化层面的删除 。
十、MMKV 数据持久化中的错误处理
10.1 文件操作错误处理
在 MMKV 数据持久化过程中,涉及众多文件操作,如文件打开、关闭、调整大小、删除、重命名等,这些操作都可能因各种原因失败,MMKV 针对这些情况进行了相应的错误处理。
- 文件打开错误:在
MMKV类的构造函数中,使用open函数打开数据文件,若打开失败,会通过perror函数记录错误信息。
// MMKV.cpp 文件中的构造函数部分代码
m_fd = open(path.c_str(), O_RDWR | O_CREAT, 0666);
if (m_fd < 0) {
// 打开文件失败,记录错误信息
perror("open");
}
- 文件大小调整错误:在文件扩容(
ensureMemorySize方法)和rewriteData方法中,使用ftruncate函数调整文件大小,若失败同样通过perror函数记录错误信息,并根据情况进行后续处理,如在扩容失败时直接返回false,在rewriteData方法中进行一些清理操作 。
// ensureMemorySize 方法中的文件大小调整部分
if (ftruncate(m_fd, m_size) != 0) {
// 调整文件大小失败,记录错误信息
perror("ftruncate");
// 重新进行内存映射
m_ptr = (char *) mmap(nullptr, oldSize, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
}
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// rewriteData 方法中的文件大小调整部分
if (ftruncate(tempFd, totalLen) != 0) {
// 调整文件大小失败,记录错误信息
perror("ftruncate temp file");
close(tempFd);
return;
}
- 文件删除、重命名错误:在
rewriteData方法中,删除原文件和重命名临时文件时,若操作失败会通过perror函数记录错误信息。
// 删除原文件
if (unlink(getMMKVPath(m_mmapID).c_str()) != 0) {
// 删除原文件失败,记录错误信息
perror("unlink original file");
}
// 重命名临时文件为原文件名
if (rename(tempPath.c_str(), getMMKVPath(m_mmapID).c_str()) != 0) {
// 重命名失败,记录错误信息
perror("rename temp file");
}
10.2 内存映射错误处理
内存映射操作(mmap 函数)在 MMKV 中至关重要,无论是初始化时映射数据文件,还是文件扩容、数据重写过程中的重新映射,若内存映射失败都会进行相应处理 。
- 初始化内存映射错误:在
MMKV类的构造函数中,若内存映射失败,会通过perror函数记录错误信息。
// MMKV.cpp 文件中的构造函数部分代码
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
}
- 文件扩容内存映射错误:在
ensureMemorySize方法中,重新进行内存映射时若失败,会通过perror函数记录错误信息,并返回false。
// ensureMemorySize 方法中的内存映射部分
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
- 数据重写内存映射错误:在
rewriteData方法中,对临时文件和重新映射原文件时,若内存映射失败,都会通过perror函数记录错误信息,并进行一些清理操作,如关闭文件等 。
// rewriteData 方法中临时文件内存映射部分
char *tempPtr = (char *) mmap(nullptr, totalLen, PROT_READ | PROT_WRITE, MAP_SHARED, tempFd, 0);
if (tempPtr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap temp file");
close(tempFd);
return;
}
// rewriteData 方法中原文件重新内存映射部分
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap original file after rename");
close(m_fd);
return;
}
10.3 其他错误处理
在数据写入、读取、删除等操作过程中,也存在一些潜在的错误情况,MMKV 也进行了相应处理。
- 数据写入错误:在
appendDataWithKey方法中,若文件扩容失败,会直接返回false,表示数据写入失败 。在同步文件内容到磁盘(msync函数)时,虽然没有显式处理返回值(msync成功时返回 0,失败返回 -1 并设置errno),但系统层面的错误会影响后续操作,若出现严重错误可能导致程序异常 。
// appendDataWithKey 方法中的文件扩容部分
if (!ensureMemorySize(m_actualSize + totalLen)) {
return false;
}
- 数据读取错误:在
loadFromFile方法中,若读取文件内容过程中出现问题,如文件格式错误等,虽然没有详细的错误处理代码,但会导致数据加载不完整或失败,进而影响后续的数据读取操作 。在getString等读取方法中,若在读取过程中出现异常(如字典查找失败等情况),会返回默认值(如空字符串) 。
// getString 方法部分代码
auto itr = m_dic.find(key);
if (itr != m_dic.end()) {
MMKVValue *value = itr->second;
if (value->type() == MMKVValue::String) {
std::string result(value->data(), value->length());
pthread_rwlock_unlock(&m_lock);
return result;
}
}
pthread_rwlock_unlock(&m_lock);
return "";
- 数据删除错误:在
rewriteData方法中,若删除原文件或重命名临时文件失败,会记录错误信息,但程序仍会继续尝试后续操作(重新打开文件、重新映射等) ,若后续操作也失败,则整个数据删除过程失败 。
10.4 错误处理流程总结
MMKV 在数据持久化过程中的错误处理遵循以下流程:
- 对于文件操作(打开、关闭、调整大小、删除、重命名等)、内存映射等关键操作,一旦失败,优先使用
perror函数记录错误信息,方便开发者排查问题 。 - 根据不同的操作场景和错误类型,进行相应的后续处理。如文件打开失败直接记录信息;文件扩容、数据写入等操作失败时返回
false,以告知调用方操作未成功 ;内存映射失败时,进行一些清理操作(如关闭文件等)并返回相应结果 。 - 在数据读取、删除等操作中,若出现错误,采取返回默认值、继续尝试后续操作等方式,尽量保证程序的稳定性,但也可能导致数据不准确或操作不完全成功 。通过这样的错误处理机制,MMKV 能够在一定程度上应对数据持久化过程中可能出现的各种错误情况,保障程序的基本运行和数据的相对完整性 。
十一、MMKV 数据持久化与 Android 系统交互
11.1 与 Android 文件系统交互
MMKV 数据持久化依赖于 Android 的文件系统,在数据存储过程中与文件系统进行了大量交互 。
- 文件路径获取:在 MMKV 初始化时,通过
context.getFilesDir()获取应用的文件目录,以此为基础构建
MMKV 数据持久化依赖于 Android 的文件系统,在数据存储过程中与文件系统进行了大量交互 。
- 文件路径获取:在 MMKV 初始化时,通过
context.getFilesDir()获取应用的文件目录,以此为基础构建 MMKV 数据文件的存储路径。以下是 Java 层获取文件目录并拼接 MMKV 存储根目录的代码:
// MMKV.java 文件中的 initialize 方法
public static String initialize(Context context) {
// 获取应用的文件目录
File root = context.getFilesDir();
// 拼接出 MMKV 的存储根目录路径
String rootDir = root.getAbsolutePath() + "/mmkv";
// 调用 native 方法进行初始化,传入存储根目录路径
nativeInitialize(rootDir);
return rootDir;
}
在 C++ 层,根据传入的存储根目录路径和 MMKV 实例的 ID 拼接出具体的数据文件路径。例如在 MMKV 类的构造函数中:
// MMKV.cpp 文件中的构造函数部分代码
MMKV::MMKV(const std::string &mmapID) : m_mmapID(mmapID) {
// 拼接数据文件的路径
std::string path = getMMKVPath(mmapID);
// ... 后续文件操作代码 ...
}
std::string MMKV::getMMKVPath(const std::string &mmapID) {
std::string path = std::string(g_rootDir) + "/" + mmapID;
return path;
}
- 文件创建与管理:在获取到文件路径后,使用标准的 C++ 文件操作函数与 Android 文件系统交互。如在构造函数中使用
open函数创建或打开数据文件,使用ftruncate函数调整文件大小,使用unlink函数删除文件,使用rename函数重命名文件等。
// 打开文件
m_fd = open(path.c_str(), O_RDWR | O_CREAT, 0666);
if (m_fd < 0) {
// 打开文件失败,记录错误信息
perror("open");
} else {
// 获取文件的状态信息
struct stat st;
if (fstat(m_fd, &st) != 0) {
// 获取文件状态失败,记录错误信息
perror("fstat");
} else {
// 获取文件的大小
m_size = st.st_size;
if (m_size == 0) {
// 如果文件大小为 0,调整文件大小为默认值
if (ftruncate(m_fd, DEFAULT_MMAP_SIZE) != 0) {
// 调整文件大小失败,记录错误信息
perror("ftruncate");
}
m_size = DEFAULT_MMAP_SIZE;
}
// ... 后续内存映射代码 ...
}
}
- 文件权限管理:在创建文件时,通过
open函数的第三个参数指定文件的权限。例如open(path.c_str(), O_RDWR | O_CREAT, 0666)表示创建的文件具有读写权限,所有者、所属组和其他用户都可以进行读写操作。这样的权限设置保证了应用对数据文件的正常访问和操作。
11.2 与 Android 进程管理交互
MMKV 支持多进程数据共享,这就需要与 Android 的进程管理机制进行交互。
- 多进程实例创建:在 Android 应用中,不同的进程可以通过
MMKV.mmkvWithID方法并指定MMKV.MULTI_PROCESS_MODE来创建支持多进程的 MMKV 实例。例如:
import com.tencent.mmkv.MMKV;
// 不同进程中创建支持多进程的 MMKV 实例
public class MultiProcessExample {
public void createMultiProcessInstance() {
// 创建支持多进程的 MMKV 实例
MMKV multiProcessMMKV = MMKV.mmkvWithID("multi_process_id", MMKV.MULTI_PROCESS_MODE);
}
}
- 进程间数据同步:MMKV 通过文件锁和内存映射机制实现进程间的数据同步。在 C++ 层,使用
pthread_rwlock_t类型的文件锁g_fileLock来保证多个进程对同一个数据文件的并发访问安全。当一个进程进行写入操作时,会先获取文件写锁,写入完成后释放锁;其他进程在读取或写入时也需要先获取相应的锁。
// MMKV.cpp 文件中的部分代码
// 初始化文件锁
pthread_rwlock_t *g_fileLock;
// 在写入数据时加文件锁
bool MMKV::setString(const char *key, const char *value) {
if (m_isMultiProcess) {
// 加文件写锁
pthread_rwlock_wrlock(g_fileLock);
}
// 加实例写锁
pthread_rwlock_wrlock(&m_lock);
// ... 写入数据的代码省略 ...
// 解锁实例写锁
pthread_rwlock_unlock(&m_lock);
if (m_isMultiProcess) {
// 解锁文件写锁
pthread_rwlock_unlock(g_fileLock);
}
return result;
}
// 在读取数据时加文件锁
std::string MMKV::getString(const char *key) {
if (m_isMultiProcess) {
// 加文件读锁
pthread_rwlock_rdlock(g_fileLock);
}
// 加实例读锁
pthread_rwlock_rdlock(&m_lock);
// ... 读取数据的代码省略 ...
// 解锁实例读锁
pthread_rwlock_unlock(&m_lock);
if (m_isMultiProcess) {
// 解锁文件读锁
pthread_rwlock_unlock(g_fileLock);
}
return result;
}
通过这种方式,不同进程对同一个 MMKV 实例进行数据读写操作时,能够保证数据的一致性和并发访问的安全性。
11.3 与 Android 内存管理交互
MMKV 使用 mmap 内存映射技术与 Android 的内存管理机制进行交互。
- 内存映射:在
MMKV类的构造函数中,使用mmap函数将数据文件映射到内存中。这样,对内存的操作就相当于对文件的操作,避免了频繁的 I/O 操作,提高了数据的读写效率。
// MMKV.cpp 文件中的构造函数部分代码
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
} else {
// 标记需要从文件加载数据
m_needLoadFromFile = true;
}
- 内存释放与回收:当文件需要扩容或进行数据重写时,会先解除原来的内存映射(使用
munmap函数),然后重新进行内存映射。例如在ensureMemorySize方法中:
// ensureMemorySize 方法中的内存映射相关代码
// 解锁
pthread_rwlock_unlock(&m_lock);
// 解除内存映射
if (munmap(m_ptr, oldSize) != 0) {
// 解除内存映射失败,记录错误信息
perror("munmap");
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// 调整文件大小
if (ftruncate(m_fd, m_size) != 0) {
// 调整文件大小失败,记录错误信息
perror("ftruncate");
// 重新进行内存映射
m_ptr = (char *) mmap(nullptr, oldSize, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
}
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// 重新进行内存映射
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return true;
通过合理的内存映射和释放操作,MMKV 能够高效地利用 Android 系统的内存资源,同时保证数据的持久化。
11.4 与 Android 系统交互总结
MMKV 在数据持久化过程中与 Android 系统的多个方面进行了紧密交互:
- 与文件系统交互,通过获取应用文件目录、创建和管理数据文件、设置文件权限等操作,确保数据能够正确地存储在文件系统中。
- 与进程管理交互,支持多进程数据共享,通过文件锁机制保证不同进程对数据的并发访问安全和数据一致性。
- 与内存管理交互,利用 mmap 内存映射技术提高数据读写效率,同时合理进行内存释放和回收,高效利用系统内存资源。这种与 Android 系统的深度交互,使得 MMKV 能够在 Android 平台上稳定、高效地实现数据持久化功能。
十二、MMKV 性能优化策略分析
12.1 mmap 内存映射优化
- 减少 I/O 操作:MMKV 使用 mmap 内存映射技术将数据文件映射到内存中,对内存的读写操作直接反映到文件上,避免了传统的文件读写需要多次进行 I/O 操作的开销。例如,在数据写入时,直接将数据写入映射的内存区域,操作系统会在合适的时候将内存中的数据同步到磁盘,减少了频繁的磁盘 I/O 操作,提高了写入性能。
// MMKV.cpp 文件中的 appendDataWithKey 方法部分代码
// 写入数据到映射的内存区域
char *ptr = m_ptr + m_actualSize;
// 写入键的长度
*((uint32_t *) ptr) = (uint32_t) keyLen;
ptr += sizeof(uint32_t);
// 写入键
memcpy(ptr, key, keyLen);
ptr += keyLen;
// 写入值的长度
*((uint32_t *) ptr) = (uint32_t) valueLen;
ptr += sizeof(uint32_t);
// 写入值
memcpy(ptr, value->data(), valueLen);
// 更新实际使用的文件大小
m_actualSize += totalLen;
// 同步文件内容到磁盘
msync(m_ptr, m_actualSize, MS_SYNC);
- 提高数据访问速度:由于数据文件被映射到内存中,程序可以直接通过内存地址访问数据,无需进行文件定位和读取等操作,大大提高了数据的访问速度。在数据读取时,直接从映射的内存区域读取数据,减少了读取时间。
// MMKV.cpp 文件中的 loadFromFile 方法部分代码
size_t offset = 0;
while (offset < m_actualSize) {
// 从映射的内存区域读取键的长度
uint32_t keyLen = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 从映射的内存区域读取键
std::string key(m_ptr + offset, keyLen);
offset += keyLen;
// 从映射的内存区域读取值的长度
uint32_t valueLen = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 从映射的内存区域读取值
MMKVValue *value = new MMKVValue(m_ptr + offset, valueLen);
offset += valueLen;
// 将键值对添加到字典中
m_dic[key] = value;
}
12.2 读写锁优化
- 并发访问控制:MMKV 使用读写锁(
pthread_rwlock_t)来控制对数据的并发访问。读写锁允许多个线程同时进行读操作,但在进行写操作时会独占锁,保证了数据的一致性和并发访问的安全性。例如在getString方法中使用读锁,允许多个线程同时读取数据:
// MMKV.cpp 文件中的 getString 方法部分代码
// 加读锁,保证并发安全
pthread_rwlock_rdlock(&m_lock);
// 检查是否需要从文件加载数据
if (m_needLoadFromFile) {
// 如果需要,加载数据
loadFromFile();
}
// 查找键对应的记录
auto itr = m_dic.find(key);
if (itr != m_dic.end()) {
// 如果找到记录,获取值
MMKVValue *value = itr->second;
if (value->type() == MMKVValue::String) {
// 如果值是字符串类型,返回字符串
std::string result(value->data(), value->length());
// 解锁
pthread_rwlock_unlock(&m_lock);
return result;
}
}
// 解锁
pthread_rwlock_unlock(&m_lock);
return "";
在 setString 方法中使用写锁,确保在写入数据时不会有其他线程同时进行读写操作:
// MMKV.cpp 文件中的 setString 方法部分代码
// 加写锁,保证并发安全
pthread_rwlock_wrlock(&m_lock);
// 查找键对应的记录
auto itr = m_dic.find(key);
if (itr != m_dic.end()) {
// 如果找到记录,删除该记录
delete itr->second;
m_dic.erase(itr);
}
// 创建新的记录
MMKVValue *newValue = new MMKVValue(value);
// 将新记录添加到字典中
m_dic[key] = newValue;
// 标记数据有更新
m_needLoadFromFile = false;
m_hasFullWriteBack = false;
// 进行数据持久化操作
bool result = appendDataWithKey(key, newValue);
// 解锁
pthread_rwlock_unlock(&m_lock);
return result;
- 减少锁竞争:通过合理的锁使用策略,减少锁的持有时间和锁竞争。例如,在
ensureMemorySize方法中,在进行文件扩容等操作时,先解锁,完成操作后再重新加锁,减少了锁的持有时间,提高了并发性能。
// ensureMemorySize 方法中的锁操作部分
// 解锁
pthread_rwlock_unlock(&m_lock);
// 解除内存映射
if (munmap(m_ptr, oldSize) != 0) {
// 解除内存映射失败,记录错误信息
perror("munmap");
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// 调整文件大小
if (ftruncate(m_fd, m_size) != 0) {
// 调整文件大小失败,记录错误信息
perror("ftruncate");
// 重新进行内存映射
m_ptr = (char *) mmap(nullptr, oldSize, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
}
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// 重新进行内存映射
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap");
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return false;
}
// 重新加锁
pthread_rwlock_wrlock(&m_lock);
return true;
12.3 文件管理优化
- 文件扩容策略:MMKV 采用了合理的文件扩容策略,当文件大小不足以存储新的数据时,每次扩容为原来的 2 倍。这样可以减少频繁的文件扩容操作,提高性能。例如在
ensureMemorySize方法中:
// ensureMemorySize 方法中的文件扩容部分
size_t oldSize = m_size;
while (m_size < newSize) {
// 每次扩容为原来的 2 倍
m_size *= 2;
}
- 数据重写优化:在删除数据后,MMKV 会进行数据重写操作,将剩余的数据重新写入文件,以保证文件的紧凑性。通过合理的内存映射和文件操作,减少了数据重写的开销。例如在
rewriteData方法中,先将数据写入临时文件,然后重命名临时文件为原文件名,避免了直接在原文件上进行大量的删除和插入操作。
// rewriteData 方法中的数据重写部分
// 临时文件路径
std::string tempPath = getMMKVPath(m_mmapID) + ".tmp";
// 打开临时文件
int tempFd = open(tempPath.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0666);
if (tempFd < 0) {
// 打开临时文件失败,记录错误信息
perror("open temp file");
return;
}
// 计算新数据总长度
size_t totalLen = 0;
for (auto itr = m_dic.begin(); itr != m_dic.end(); ++itr) {
const char *key = itr->first;
MMKVValue *value = itr->second;
size_t keyLen = strlen(key);
size_t valueLen = value->length();
totalLen += 2 * sizeof(uint32_t) + keyLen + valueLen;
}
// 调整临时文件大小
if (ftruncate(tempFd, totalLen) != 0) {
// 调整文件大小失败,记录错误信息
perror("ftruncate temp file");
close(tempFd);
return;
}
// 内存映射临时文件
char *tempPtr = (char *) mmap(nullptr, totalLen, PROT_READ | PROT_WRITE, MAP_SHARED, tempFd, 0);
if (tempPtr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap temp file");
close(tempFd);
return;
}
// 写入数据到临时文件
char *writePtr = tempPtr;
for (auto itr = m_dic.begin(); itr != m_dic.end(); ++itr) {
const char *key = itr->first;
MMKVValue *value = itr->second;
size_t keyLen = strlen(key);
size_t valueLen = value->length();
// 写入键的长度
*((uint32_t *) writePtr) = (uint32_t) keyLen;
writePtr += sizeof(uint32_t);
// 写入键
memcpy(writePtr, key, keyLen);
writePtr += keyLen;
// 写入值的长度
*((uint32_t *) writePtr) = (uint32_t) valueLen;
writePtr += sizeof(uint32_t);
// 写入值
memcpy(writePtr, value->data(), valueLen);
writePtr += valueLen;
}
// 同步临时文件内容到磁盘
msync(tempPtr, totalLen, MS_SYNC);
// 解除临时文件内存映射
if (munmap(tempPtr, totalLen) != 0) {
// 解除内存映射失败,记录错误信息
perror("munmap temp file");
}
// 关闭临时文件
close(tempFd);
// 关闭原文件
close(m_fd);
// 删除原文件
if (unlink(getMMKVPath(m_mmapID).c_str()) != 0) {
// 删除原文件失败,记录错误信息
perror("unlink original file");
}
// 重命名临时文件为原文件名
if (rename(tempPath.c_str(), getMMKVPath(m_mmapID).c_str()) != 0) {
// 重命名失败,记录错误信息
perror("rename temp file");
}
// 重新打开文件
m_fd = open(getMMKVPath(m_mmapID).c_str(), O_RDWR | O_CREAT, 0666);
if (m_fd < 0) {
// 打开文件失败,记录错误信息
perror("open original file after rename");
return;
}
// 获取文件状态信息
struct stat st;
if (fstat(m_fd, &st) != 0) {
// 获取文件状态失败,记录错误信息
perror("fstat original file after rename");
close(m_fd);
return;
}
// 获取文件大小
m_size = st.st_size;
// 重新进行内存映射
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
// 内存映射失败,记录错误信息
perror("mmap original file after rename");
close(m_fd);
return;
}
// 更新实际使用的文件大小
m_actualSize = m_size;
12.4 性能优化总结
MMKV 通过多种性能优化策略提高了数据持久化的性能:
- mmap 内存映射技术减少了 I/O 操作,提高了数据访问速度。
- 读写锁机制实现了并发访问控制,减少了锁竞争。
- 合理的文件管理策略,如文件扩容和数据重写优化,减少了文件操作的开销。这些优化策略使得 MMKV 在数据读写性能上优于传统的 Android 数据持久化方案,能够更好地满足应用的性能需求。
十三、MMKV 与其他 Android 数据持久化方案对比
13.1 与 SharedPreferences 对比
- 性能对比
- 读写速度:
SharedPreferences是 Android 系统提供的一种轻量级的键值对存储方式,其读写操作基于 XML 文件,在数据量较小的情况下性能尚可,但当数据量增大时,读写速度会明显变慢。因为每次读写操作都需要进行 XML 文件的解析和序列化,涉及大量的 I/O 操作。而 MMKV 使用 mmap 内存映射技术,避免了频繁的 I/O 操作,读写速度更快。例如,在大量数据写入时,MMKV 的写入性能远高于SharedPreferences。 - 并发性能:
SharedPreferences在多进程环境下存在并发问题,需要额外的同步机制来保证数据的一致性。而 MMKV 天生支持多进程,通过文件锁机制保证了多进程对数据的并发访问安全,并发性能更好。
- 读写速度:
- 数据存储格式对比
SharedPreferences使用 XML 格式存储数据,XML 文件的解析和序列化会带来一定的性能开销,并且在存储复杂数据类型时不够灵活。而 MMKV 使用自定义的二进制格式存储数据,存储效率更高,能够更好地支持各种数据类型。
- 使用便捷性对比
SharedPreferences的 API 简单易用,对于简单的键值对存储场景非常方便。但在处理复杂数据和多进程数据共享时,需要编写额外的代码。MMKV 提供了与SharedPreferences类似的 API,同时在多进程支持和性能方面更具优势,使用起来也比较便捷。
13.2 与 SQLite 对比
- 性能对比
- 读写速度:SQLite 是一种嵌入式数据库,适用于存储大量结构化数据。在数据量较小的情况下,SQLite 的读写操作会有一定的开销,因为需要进行 SQL 语句的解析和执行。而 MMKV 在处理简单的键值对数据时,读写速度更快,因为它直接操作内存映射的文件,避免了 SQL 语句的解析和执行过程。
- 并发性能:SQLite 在多线程和多进程环境下需要复杂的同步机制来保证数据的一致性,否则可能会出现数据冲突问题。MMKV 通过文件锁机制实现了简单高效的并发控制,在多进程环境下的并发性能更优。
- 数据存储结构对比
- SQLite 是一种关系型数据库,需要定义表结构和字段,适合存储结构化的数据。而 MMKV 是一种键值对存储方案,不需要定义复杂的表结构,对于简单的键值对存储更加方便。
- 使用场景对比
- SQLite 适用于存储大量结构化数据,如联系人信息、新闻列表等,并且支持复杂的查询操作。而 MMKV 更适合存储简单的配置信息、缓存数据等,对于需要快速读写的键值对数据场景更为合适。
13.3 对比总结
MMKV 在性能、并发支持和使用便捷性等方面具有一定的优势:
- 与
SharedPreferences相比,MMKV 在读写速度、并发性能和数据存储格式上更具优势,能够更好地应对大数据量和多进程场景。 - 与 SQLite 相比,MMKV 在处理简单键值对数据时读写速度更快,并发控制更简单,适合简单的配置和缓存数据存储。但 SQLite 在存储结构化数据和复杂查询方面具有优势。开发者可以根据具体的应用场景选择合适的数据持久化方案。
十四、MMKV 在实际项目中的应用案例
14.1 案例一:用户配置信息存储
- 需求背景:在一个社交应用中,需要存储用户的一些配置信息,如主题颜色、消息提醒开关、字体大小等。这些配置信息需要在应用启动时快速加载,并且支持多进程共享,以便在不同的进程中都能及时获取到最新的配置信息。
- 解决方案:使用 MMKV 来存储用户的配置信息。在应用启动时,通过
MMKV.defaultMMKV()获取默认的 MMKV 实例,然后读取存储的配置信息。在用户修改配置信息时,调用encode方法将新的配置信息写入 MMKV。
import com.tencent.mmkv.MMKV;
public class UserConfigManager {
private static final String KEY_THEME_COLOR = "theme_color";
private static final String KEY_MESSAGE_REMIND = "message_remind";
private static final String KEY_FONT_SIZE = "font_size";
private MMKV mmkv;
public UserConfigManager() {
// 获取默认的 MMKV 实例
mmkv = MMKV.defaultMMKV();
}
// 获取主题颜色
public int getThemeColor() {
return mmkv.decodeInt(KEY_THEME_COLOR, 0xFF0000);
}
// 设置主题颜色
public void setThemeColor(int color) {
mmkv.encode(KEY_THEME_COLOR, color);
}
// 获取消息提醒开关状态
public boolean isMessageRemindEnabled() {
return mmkv.decodeBool(KEY_MESSAGE_REMIND, true);
}
// 设置消息提醒开关状态
public void setMessageRemindEnabled(boolean enabled) {
mmkv.encode(KEY_MESSAGE_REMIND, enabled);
}
// 获取字体大小
public int getFontSize() {
return mmkv.decodeInt(KEY_FONT_SIZE, 16);
}
// 设置字体大小
public void setFontSize(int size) {
mmkv.encode(KEY_FONT_SIZE, size);
}
}
- 效果评估:使用 MMKV 存储用户配置信息后,应用启动时能够快速加载配置信息,提高了应用的响应速度。同时,由于 MMKV 支持多进程共享,不同进程中能够及时获取到最新的配置信息,保证了用户体验的一致性。
14.2 案例二:缓存数据存储
- 需求背景:在一个新闻资讯应用中,需要缓存新闻列表数据,以减少网络请求,提高应用的响应速度。缓存的数据需要在一定时间内有效,并且能够快速读写。
- 解决方案:使用 MMKV 来缓存新闻列表数据。在获取新闻列表数据时,将数据以 JSON 字符串的形式存储到 MMKV 中,并记录存储时间。在下次获取新闻列表时,先检查缓存数据的有效性,如果有效则直接从 MMKV 中读取数据,否则重新从网络获取数据。
import com.tencent.mmkv.MMKV;
import org.json.JSONArray;
import java.util.Date;
public class NewsCacheManager {
private static final String KEY_NEWS_LIST = "news_list";
private static final String KEY_CACHE_TIME = "cache_time";
private static final long CACHE_VALID_TIME = 60 * 60 * 1000; // 缓存有效时间为 1 小时
private MMKV mmkv;
public NewsCacheManager() {
// 获取默认的 MMKV 实例
mmkv = MMKV.defaultMMKV();
}
// 缓存新闻列表数据
public void cacheNewsList(JSONArray newsList) {
String newsListJson = newsList.toString();
mmkv.encode(KEY_NEWS_LIST, newsListJson);
mmkv.encode(KEY_CACHE_TIME, new Date().getTime());
}
// 获取缓存的新闻列表数据
public JSONArray getCachedNewsList() {
long cacheTime = mmkv.decodeLong(KEY_CACHE_TIME, 0);
long currentTime = new Date().getTime();
if (currentTime - cacheTime < CACHE_VALID_TIME) {
String newsListJson = mmkv.decodeString(KEY_NEWS_LIST);
if (newsListJson != null) {
try {
return new JSONArray(newsListJson);
} catch (Exception e) {
e.printStackTrace();
}
}
}
return null;
}
}
- 效果评估:通过使用 MMKV 缓存新闻列表数据,减少了网络请求次数,提高了应用的响应速度。同时,由于 MMKV 的读写性能较高,能够快速地读取和写入缓存数据,提升了用户体验。
14.3 案例三:多进程数据共享
- 需求背景:在一个电商应用中,主进程和推送进程需要共享一些数据,如用户的登录状态、购物车数量等。这些数据需要在不同进程中实时同步,以保证数据的一致性。
- 解决方案:使用 MMKV 的多进程模式来实现数据共享。在主进程和推送进程中,通过
MMKV.mmkvWithID方法并指定MMKV.MULTI_PROCESS_MODE创建支持多进程的 MMKV 实例。在主进程中更新数据时,直接调用encode方法;在推送进程中读取数据时,直接调用decode方法。
import com.tencent.mmkv.MMKV;
// 主进程更新数据
public class MainProcess {
private static final String MMKV_ID = "multi_process_data";
private static final String KEY_LOGIN_STATUS = "login_status";
private static final String KEY_CART_COUNT = "cart_count";
public void updateData() {
// 创建支持多进程的 MMKV 实例
MMKV multiProcessMMKV = MMKV.mmkvWithID(MMKV_ID, MMKV.MULTI_PROCESS_MODE);
// 更新登录状态
multiProcessMMKV.encode(KEY_LOGIN_STATUS, true);
// 更新购物车数量
multiProcessMMKV.encode(KEY_CART_COUNT, 5);
}
}
// 推送进程读取数据
public class PushProcess {
private static final String MMKV_ID = "multi_process_data";
private static final String KEY_LOGIN_STATUS = "login_status";
private static final String KEY_CART_COUNT = "cart_count";
public void readData() {
// 创建支持多进程的 MMKV 实例
MMKV multiProcessMMKV = MMKV.mmkvWithID(MMKV_ID, MMKV.MULTI_PROCESS_MODE);
// 读取登录状态
boolean loginStatus = multiProcessMMKV.decodeBool(KEY_LOGIN_STATUS, false);
// 读取购物车数量
int cartCount = multiProcessMMKV.decodeInt(KEY_CART_COUNT, 0);
}
}
- 效果评估:通过使用 MMKV 的多进程模式,实现了主进程和推送进程之间的数据实时同步,保证了数据的一致性。同时,由于 MMKV 的多进程支持性能较好,在数据同步过程中没有出现明显的延迟和数据冲突问题。
14.4 应用案例总结
通过以上实际项目中的应用案例可以看出,MMKV 在不同的应用场景中都能够发挥出其优势:
- 在用户配置信息存储场景中,能够快速读写数据,支持多进程共享,提高应用的响应速度和用户体验。
- 在缓存数据存储场景中,能够高效地缓存和读取数据,减少网络请求,提升应用性能。
- 在多进程数据共享场景中,能够实现不同进程之间的数据实时同步,保证数据的一致性。开发者可以根据具体的项目需求,合理使用 MMKV 来实现数据持久化功能。