深度揭秘!Android MMKV数据更新与删除的底层实现全解析
一、引言
在Android应用开发中,数据的动态管理是不可或缺的环节。作为高性能的键值对存储框架,MMKV凭借其高效的存储和读取性能受到广泛关注。除了基础的存储与读取功能,MMKV在数据更新与删除操作上同样有着精妙的设计。深入理解MMKV如何实现数据的更新与删除,不仅有助于开发者更好地优化应用性能,还能为解决实际开发中遇到的问题提供思路。本文将从源码级别对MMKV的数据更新与删除机制进行全面剖析,带你一探其底层实现的奥秘。
二、MMKV基础回顾
2.1 MMKV简介
MMKV(Multi - Process Key - Value)是腾讯开源的高性能Android键值对存储框架。它基于mmap内存映射技术和Protobuf数据编码,实现了高效的数据存储与读取。与传统的SharedPreferences相比,MMKV在性能和多进程支持方面具有显著优势,特别适合处理频繁的数据操作场景。
2.2 MMKV初始化
在使用MMKV进行数据操作前,需要先进行初始化。初始化过程主要包括确定数据存储目录和完成底层资源的准备工作。
// 在Application的onCreate方法中进行初始化
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化MMKV,返回存储根目录
String rootDir = MMKV.initialize(this);
// 打印初始化后的根目录
android.util.Log.d("MMKV", "MMKV root: " + rootDir);
}
}
在MMKV.java中,initialize方法的实现如下:
public static String initialize(Context context) {
// 获取应用的内部文件目录
File root = context.getFilesDir();
// 在内部文件目录下创建mmkv子目录
File rootDir = new File(root, "mmkv");
// 调用native方法进行初始化
return initialize(rootDir.getAbsolutePath());
}
// 调用底层C++的初始化方法
private static native String initialize(String rootDir);
在C++层的MMKV.cpp中,initializeMMKV方法负责具体的初始化工作:
void initializeMMKV(const std::string &rootDir) {
// 检查根目录是否为空
if (rootDir.empty()) {
return;
}
// 设置全局根目录
MMKV::g_rootDir = rootDir;
// 创建根目录
mkdir(MMKV::g_rootDir.c_str(), 0777);
// 初始化线程锁
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&MMKV::g_instanceLock, &attr);
pthread_mutexattr_destroy(&attr);
}
初始化完成后,MMKV就准备好进行数据的存储、更新和删除操作了。
三、MMKV数据更新原理
3.1 数据更新流程概述
MMKV的数据更新操作本质上是先删除旧数据,再写入新数据。具体流程如下:
- 在内存映射区域中查找要更新的键对应的数据。
- 删除旧数据。
- 对新数据进行编码。
- 将编码后的新数据写入内存映射区域。
- 在合适的时机将内存中的数据同步到文件。
3.2 数据查找
在进行数据更新前,首先需要找到要更新的数据。MMKV通过遍历内存映射区域来查找指定键的数据。
// MMKV.cpp中查找数据的方法
std::string MMKV::getData(const std::string &key) {
// 加锁,保证线程安全
SCOPEDLOCK(m_lock);
// 从内存映射区域的起始位置开始查找
size_t offset = 0;
while (offset < m_actualSize) {
// 读取当前数据项的键长度
uint32_t keyLength = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 读取键的内容
std::string currentKey(m_ptr + offset, keyLength);
offset += keyLength;
// 读取值的长度
uint32_t valueLength = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 如果找到匹配的键
if (currentKey == key) {
// 返回对应的值
return std::string(m_ptr + offset, valueLength);
}
// 跳过当前值,继续查找
offset += valueLength;
}
// 未找到匹配的键,返回空字符串
return "";
}
3.3 旧数据删除
找到要更新的数据后,需要将其从内存映射区域中删除。删除操作实际上是通过调整数据存储的偏移量和实际使用大小来实现逻辑删除。
// MMKV.cpp中删除数据的方法
bool MMKV::removeData(const std::string &key) {
// 加锁,保证线程安全
SCOPEDLOCK(m_lock);
// 记录上一次的偏移量
size_t lastOffset = 0;
// 当前偏移量
size_t offset = 0;
while (offset < m_actualSize) {
// 读取当前数据项的键长度
uint32_t keyLength = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 读取键的内容
std::string currentKey(m_ptr + offset, keyLength);
offset += keyLength;
// 读取值的长度
uint32_t valueLength = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 如果找到匹配的键
if (currentKey == key) {
// 计算删除当前数据项后的新实际大小
size_t newActualSize = m_actualSize - (offset - lastOffset + valueLength);
// 将后面的数据向前移动,覆盖要删除的数据
memmove(m_ptr + lastOffset, m_ptr + offset + valueLength, m_actualSize - offset);
// 更新实际使用大小
m_actualSize = newActualSize;
// 标记数据已修改
m_dirty = true;
return true;
}
// 更新上一次的偏移量
lastOffset = offset;
// 跳过当前值
offset += valueLength;
}
// 未找到匹配的键,删除失败
return false;
}
3.4 新数据编码与写入
删除旧数据后,需要对新数据进行编码,并将其写入内存映射区域。以更新字符串数据为例:
// MMKV.cpp中更新字符串数据的方法
bool MMKV::putString(const std::string &key, const std::string &value) {
// 先删除旧数据
removeData(key);
// 创建Protobuf的Writer对象用于编码
CodedOutputStream::ArrayOutputStream aos;
CodedOutputStream cos(&aos);
// 写入字符串类型标识
cos.WriteVarint32(ProtobufType_String);
// 写入字符串长度
cos.WriteVarint32(value.length());
// 写入字符串内容
cos.WriteRaw(value.data(), value.length());
// 获取编码后的字节数组
std::string encodedValue = aos.GetBufferAsString();
// 调用putData方法写入新数据
return putData(key, encodedValue);
}
putData方法负责将编码后的数据写入内存映射区域:
// MMKV.cpp中写入数据的方法
bool MMKV::putData(const std::string &key, const std::string &value) {
// 加锁,保证线程安全
SCOPEDLOCK(m_lock);
// 记录当前实际使用大小
size_t oldSize = m_actualSize;
// 计算新数据的总长度
size_t newSize = oldSize + value.length() + key.length() + 2 * sizeof(uint32_t);
// 检查是否需要扩容
if (newSize > m_size) {
// 进行扩容操作
if (!ensureMemorySize(newSize)) {
return false;
}
}
// 获取内存映射区域的写入指针
char *ptr = m_ptr + oldSize;
// 写入键的长度
*((uint32_t *) ptr) = (uint32_t) key.length();
ptr += sizeof(uint32_t);
// 写入键的内容
memcpy(ptr, key.data(), key.length());
ptr += key.length();
// 写入值的长度
*((uint32_t *) ptr) = (uint32_t) value.length();
ptr += sizeof(uint32_t);
// 写入值的内容
memcpy(ptr, value.data(), value.length());
// 更新实际使用大小
m_actualSize = newSize;
// 标记数据已修改
m_dirty = true;
return true;
}
3.5 文件同步
为了保证数据的持久化,MMKV会在合适的时机将内存中的数据同步到文件。
// MMKV.cpp中同步数据到文件的方法
bool MMKV::sync() {
// 加锁,保证线程安全
SCOPEDLOCK(m_lock);
// 如果数据没有修改,无需同步
if (!m_dirty) {
return true;
}
// 使用msync将内存数据同步到文件
if (msync(m_ptr, m_actualSize, MS_SYNC) != 0) {
return false;
}
// 标记数据已同步
m_dirty = false;
return true;
}
四、MMKV数据删除原理
4.1 数据删除流程概述
MMKV的数据删除操作相对直接,主要流程如下:
- 在内存映射区域中查找要删除的键对应的数据。
- 删除找到的数据。
- 在合适的时机将内存中的数据同步到文件,完成持久化删除。
4.2 数据查找与删除
数据查找的过程与更新操作中的查找过程相同,通过遍历内存映射区域找到要删除的键对应的数据。找到数据后,调用removeData方法进行删除:
// MMKV.cpp中删除数据的方法
bool MMKV::removeData(const std::string &key) {
// 加锁,保证线程安全
SCOPEDLOCK(m_lock);
// 记录上一次的偏移量
size_t lastOffset = 0;
// 当前偏移量
size_t offset = 0;
while (offset < m_actualSize) {
// 读取当前数据项的键长度
uint32_t keyLength = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 读取键的内容
std::string currentKey(m_ptr + offset, keyLength);
offset += keyLength;
// 读取值的长度
uint32_t valueLength = *((uint32_t *) (m_ptr + offset));
offset += sizeof(uint32_t);
// 如果找到匹配的键
if (currentKey == key) {
// 计算删除当前数据项后的新实际大小
size_t newActualSize = m_actualSize - (offset - lastOffset + valueLength);
// 将后面的数据向前移动,覆盖要删除的数据
memmove(m_ptr + lastOffset, m_ptr + offset + valueLength, m_actualSize - offset);
// 更新实际使用大小
m_actualSize = newActualSize;
// 标记数据已修改
m_dirty = true;
return true;
}
// 更新上一次的偏移量
lastOffset = offset;
// 跳过当前值
offset += valueLength;
}
// 未找到匹配的键,删除失败
return false;
}
4.3 文件同步
数据删除后,同样需要将内存中的修改同步到文件,确保数据的持久化删除。
// MMKV.cpp中同步数据到文件的方法
bool MMKV::sync() {
// 加锁,保证线程安全
SCOPEDLOCK(m_lock);
// 如果数据没有修改,无需同步
if (!m_dirty) {
return true;
}
// 使用msync将内存数据同步到文件
if (msync(m_ptr, m_actualSize, MS_SYNC) != 0) {
return false;
}
// 标记数据已同步
m_dirty = false;
return true;
}
五、多进程环境下的数据更新与删除
5.1 多进程数据操作的挑战
在多进程环境下,数据的更新和删除操作面临数据一致性和并发访问的挑战。多个进程同时对同一份数据进行操作时,可能会导致数据混乱或丢失。
5.2 MMKV的多进程支持
MMKV通过文件锁机制来保证多进程环境下的数据一致性。在进行数据更新或删除操作前,先获取文件锁,操作完成后再释放文件锁。
// MMKV.cpp中用于多进程写入的方法
bool MMKV::putDataWithLock(const std::string &key, const std::string &value) {
// 获取文件写锁
if (!m_fileLock.lockWrite()) {
return false;
}
// 调用putData方法进行数据写入
bool result = putData(key, value);
// 释放文件写锁
m_fileLock.unlockWrite();
return result;
}
// MMKV.cpp中用于多进程删除的方法
bool MMKV::removeDataWithLock(const std::string &key) {
// 获取文件写锁
if (!m_fileLock.lockWrite()) {
return false;
}
// 调用removeData方法进行数据删除
bool result = removeData(key);
// 释放文件写锁
m_fileLock.unlockWrite();
return result;
}
5.3 多进程数据一致性保障
通过文件锁机制,MMKV确保在同一时刻只有一个进程能够对数据进行写入或删除操作,从而保证了多进程环境下的数据一致性。
六、性能优化与注意事项
6.1 性能优化
- 批量操作:尽量将多个更新或删除操作合并为一次批量操作,减少锁的获取和释放次数,提高性能。
- 减少不必要的操作:在进行数据更新前,先判断数据是否真的需要更新,避免无效的写入操作。
6.2 注意事项
- 数据覆盖风险:在进行数据更新时,要确保操作的准确性,避免误覆盖重要数据。
- 多进程冲突:在多进程环境下,要合理使用文件锁,避免因锁竞争导致性能下降或死锁问题。
- 内存占用:频繁的更新和删除操作可能会导致内存碎片,影响性能。可以通过适当的内存管理策略来缓解这个问题。