深入理解Android MMKV初始化流程:从Java到C++全解析
一、MMKV概述
1.1 MMKV简介
MMKV是腾讯开源的高性能键值存储框架,基于内存映射文件实现,相比传统的SharedPreferences具有显著的性能优势。它专为移动应用设计,特别适合高频次的小数据存储场景。
1.2 核心优势
- 高性能:基于内存映射文件,读写操作无需序列化
- 线程安全:支持多线程并发访问
- 数据加密:支持AES加密
- 体积小巧:库文件体积小,引入成本低
1.3 应用场景
- 高频次的配置数据存储
- 用户偏好设置
- 临时数据缓存
二、MMKV初始化入口
2.1 Java层初始化API
MMKV的初始化通常从获取实例开始,最常用的方法是defaultMMKV():
// MMKV.java
/**
* 获取默认的MMKV实例
* @return 默认的MMKV实例
*/
public static MMKV defaultMMKV() {
// 调用带标志位的defaultMMKV方法,使用默认标志位
return defaultMMKV(MMKV.SINGLE_PROCESS_MODE, null);
}
/**
* 获取默认的MMKV实例,指定标志位和加密密钥
* @param mode 进程模式,如SINGLE_PROCESS_MODE或MULTI_PROCESS_MODE
* @param cryptKey 加密密钥,可为null
* @return 默认的MMKV实例
*/
public static synchronized MMKV defaultMMKV(int mode, String cryptKey) {
if (sDefaultMMKVPath == null) {
// 如果默认路径未初始化,抛出异常
throw new IllegalStateException("You should call MMKV.initialize() first.");
}
// 调用getInstance方法获取实例,使用默认路径
return getInstance(DEFAULT_MMKV_ID, sDefaultMMKVPath, mode, cryptKey);
}
2.2 初始化环境准备
在使用MMKV之前,需要先调用initialize()方法进行环境准备:
// MMKV.java
/**
* 初始化MMKV库
* @param rootDir 存储MMKV文件的根目录
* @return 初始化是否成功
*/
public static boolean initialize(Context context) {
// 获取应用的文件目录作为根目录
File root = context.getFilesDir();
String rootPath = root.getAbsolutePath() + "/mmkv";
// 调用带根目录的initialize方法
return initialize(rootPath);
}
/**
* 初始化MMKV库,指定根目录
* @param rootDir 存储MMKV文件的根目录
* @return 初始化是否成功
*/
public static boolean initialize(String rootDir) {
// 加载本地库
try {
System.loadLibrary("mmkv");
} catch (UnsatisfiedLinkError e) {
e.printStackTrace();
return false;
}
// 存储默认路径
sDefaultMMKVPath = rootDir;
// 调用本地方法进行初始化
nativeInitialize(rootDir);
return true;
}
2.3 关键参数说明
- rootDir:MMKV文件的存储根目录
- mode:进程模式,支持单进程和多进程模式
- cryptKey:加密密钥,用于数据加密
三、Java层初始化流程
3.1 实例获取流程
当调用getInstance()方法时,MMKV会检查是否已有缓存的实例:
// MMKV.java
/**
* 获取指定ID的MMKV实例
* @param mmkvID MMKV实例的唯一标识
* @param rootPath 存储根目录
* @param mode 进程模式
* @param cryptKey 加密密钥
* @return MMKV实例
*/
public static synchronized MMKV getInstance(String mmkvID, String rootPath, int mode, String cryptKey) {
// 构建实例的唯一键
String fullPath = rootPath + "/" + mmkvID;
String cryptKeyWithPath = fullPath + cryptKey;
// 从缓存中查找实例
MMKV mmkv = sInstanceMap.get(cryptKeyWithPath);
if (mmkv != null) {
// 如果实例已存在,直接返回
return mmkv;
}
// 创建新的MMKV实例
mmkv = new MMKV(mmkvID, rootPath, mode, cryptKey);
// 加入缓存
sInstanceMap.put(cryptKeyWithPath, mmkv);
return mmkv;
}
3.2 构造函数实现
MMKV的构造函数会初始化基本参数并调用本地方法创建C++实例:
// MMKV.java
/**
* 私有构造函数,创建MMKV实例
*/
private MMKV(String mmkvID, String rootPath, int mode, String cryptKey) {
// 保存参数
m_mmkvID = mmkvID;
m_rootPath = rootPath;
m_mode = mode;
m_cryptKey = cryptKey;
// 调用本地方法创建C++实例
m_nativeHandle = nativeCreate(mmkvID, rootPath, mode, cryptKey);
if (m_nativeHandle == 0) {
throw new IllegalStateException("MMKV create failed");
}
}
3.3 静态代码块
MMKV类的静态代码块会在类加载时执行一些初始化工作:
// MMKV.java
static {
// 初始化日志回调
nativeSetupLogging(nLogLevel);
// 设置异常处理回调
nativeSetCrashHandler();
}
四、JNI层初始化流程
4.1 JNI方法映射
MMKV通过JNI将Java方法映射到C++方法:
// Jni.cpp
static JNINativeMethod gMethods[] = {
// 映射Java层的nativeCreate方法到C++层的jniCreateMMKV方法
{"nativeCreate", "(Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)J", (void *) jniCreateMMKV},
// 映射Java层的nativeInitialize方法到C++层的jniInitializeMMKV方法
{"nativeInitialize", "(Ljava/lang/String;)V", (void *) jniInitializeMMKV},
// 其他方法映射...
};
// 注册JNI方法
jint register_com_tencent_mmkv_MMKV(JNIEnv *env) {
return jniRegisterNativeMethods(env, "com/tencent/mmkv/MMKV",
gMethods, NELEM(gMethods));
}
4.2 环境初始化
jniInitializeMMKV方法会初始化MMKV的运行环境:
// Jni.cpp
static void jniInitializeMMKV(JNIEnv *env, jobject, jstring rootDir) {
const char *rootPath = env->GetStringUTFChars(rootDir, nullptr);
if (rootPath) {
// 调用MMKV的initializeMMKV方法进行初始化
MMKV::initializeMMKV(rootPath);
env->ReleaseStringUTFChars(rootDir, rootPath);
}
}
4.3 实例创建
jniCreateMMKV方法会创建C++层的MMKV实例:
// Jni.cpp
static jlong jniCreateMMKV(JNIEnv *env, jobject, jstring mmkvID, jstring rootDir, jint mode, jstring cryptKeyStr) {
const char *mmkvIDStr = env->GetStringUTFChars(mmkvID, nullptr);
const char *rootPath = env->GetStringUTFChars(rootDir, nullptr);
const char *cryptKey = nullptr;
if (cryptKeyStr) {
cryptKey = env->GetStringUTFChars(cryptKeyStr, nullptr);
}
// 调用MMKV::mmkvWithID方法创建实例
MMKV *mmkv = MMKV::mmkvWithID(mmkvIDStr, (MMKVMode) mode, cryptKey, rootPath);
if (cryptKey) {
env->ReleaseStringUTFChars(cryptKeyStr, cryptKey);
}
env->ReleaseStringUTFChars(rootDir, rootPath);
env->ReleaseStringUTFChars(mmkvID, mmkvIDStr);
// 将C++实例指针转换为jlong返回给Java层
return (jlong) mmkv;
}
五、C++层初始化流程
5.1 全局初始化
initializeMMKV方法会进行全局环境的初始化:
// MMKV.cpp
void MMKV::initializeMMKV(const char *rootDir) {
// 初始化单例管理器
MMKVInstanceManager::getInstance()->initialize(rootDir);
// 设置日志函数
setLogHandler(gDefaultLogHandler);
// 初始化随机数生成器
randomizeSeed();
}
5.2 实例管理器初始化
MMKVInstanceManager负责管理所有MMKV实例:
// MMKVInstanceManager.cpp
void MMKVInstanceManager::initialize(const char *rootDir) {
SCOPED_LOCK(m_lock);
// 保存根目录
m_rootDir = rootDir;
// 创建根目录
createDirectory(m_rootDir.c_str());
// 初始化进程锁
string lockPath = m_rootDir + "/mmkv.lock";
m_processLock = new FileLock(lockPath.c_str());
// 加锁以确保全局初始化的原子性
m_processLock->lock();
// 检查并修复可能的文件损坏
checkFileCorruption();
// 解锁
m_processLock->unlock();
}
5.3 实例创建
mmkvWithID方法会创建具体的MMKV实例:
// MMKV.cpp
MMKV *MMKV::mmkvWithID(const char *mmapID, MMKVMode mode, const char *cryptKey, const char *rootPath) {
// 获取实例管理器
auto instanceManager = MMKVInstanceManager::getInstance();
// 构建完整路径
string finalRoot = rootPath ? rootPath : instanceManager->m_rootDir;
string fullPath = finalRoot + "/" + mmapID;
// 加锁以确保线程安全
SCOPED_LOCK(instanceManager->m_lock);
// 从缓存中查找实例
auto itr = instanceManager->m_instanceDic.find(fullPath);
if (itr != instanceManager->m_instanceDic.end()) {
// 如果实例已存在,返回缓存的实例
MMKV *kv = itr->second;
if (kv) {
return kv;
}
}
// 创建新的MMKV实例
auto kv = new MMKV(fullPath, mode, cryptKey);
// 加入缓存
instanceManager->m_instanceDic[fullPath] = kv;
return kv;
}
六、文件初始化流程
6.1 文件路径处理
MMKV会根据实例ID构建完整的文件路径:
// MMKV.cpp
MMKV::MMKV(const string &path, MMKVMode mode, const string &cryptKey)
: m_path(path)
, m_crcPath(path + ".crc")
, m_lockPath(path + ".lock")
, m_mode(mode)
, m_crypter(nullptr)
, m_dic(nullptr)
, m_output(nullptr)
, m_crcDigest(0)
, m_actualSize(0)
, m_fileLength(0)
, m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0)
, m_needLoadFromFile(true)
, m_hasFullWriteback(false)
, m_dirty(false) {
// 创建目录(如果不存在)
createParentDirectory(m_path);
// 初始化文件锁
m_fileLock = new FileLock(m_lockPath);
// 如果是多进程模式,使用进程间锁
if (m_isInterProcess) {
m_sharedProcessLock = m_fileLock->createSharedLock();
m_exclusiveProcessLock = m_fileLock->createExclusiveLock();
}
// 初始化内存锁
m_lock = new PThreadLock();
m_sharedLock = m_lock->createSharedLock();
m_exclusiveLock = m_lock->createExclusiveLock();
// 如果提供了加密密钥,初始化加密器
if (!cryptKey.empty()) {
m_crypter = new AESCrypter((const unsigned char *) cryptKey.data(), (int) cryptKey.length());
}
// 初始化文件映射
initializeMmapedFile();
}
6.2 文件映射初始化
initializeMmapedFile方法会进行文件映射的初始化:
// MMKV.cpp
void MMKV::initializeMmapedFile() {
// 加进程间排他锁,确保同一时间只有一个进程可以初始化文件
SCOPED_LOCK_EXCLUSIVE_PROCESS;
// 打开文件
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 {
// 获取文件长度
struct stat st = {0};
if (fstat(m_fd, &st) != -1) {
m_fileLength = (size_t) st.st_size;
}
// 确保文件长度至少为一个header的大小
if (m_fileLength < Fixed32Size) {
// 文件为空,写入初始header
size_t size = DEFAULT_MMAP_SIZE;
truncate(m_fd, (off_t) size);
m_fileLength = size;
// 写入初始header
uint32_t header = 0;
pwrite(m_fd, &header, Fixed32Size, 0);
// 更新CRC校验
updateCRCDigest(nullptr, 0, true);
// 保存CRC校验值
saveCRCDigest();
m_actualSize = 0;
m_dirty = true;
} else {
// 文件已存在,加载数据
loadFromFile();
}
// 映射文件到内存
mapFile();
}
}
6.3 文件加载
loadFromFile方法会从文件中加载数据:
// MMKV.cpp
bool MMKV::loadFromFile() {
// 读取CRC校验值
uint32_t storedCRC = 0;
{
int crcFd = open(m_crcPath.c_str(), O_RDONLY);
if (crcFd >= 0) {
read(crcFd, &storedCRC, Fixed32Size);
close(crcFd);
}
}
// 读取文件内容
size_t size = m_fileLength;
auto buffer = new (std::nothrow) unsigned char[size];
if (!buffer) {
return false;
}
// 读取header
size_t headerSize = Fixed32Size;
pread(m_fd, buffer, headerSize, 0);
// 解析header
uint32_t header = *(uint32_t *) buffer;
m_actualSize = (header & 0x0FFFFFFF);
// 读取实际数据
size_t dataSize = 0;
if (m_actualSize + headerSize <= size) {
dataSize = m_actualSize;
if (dataSize > 0) {
pread(m_fd, buffer + headerSize, dataSize, headerSize);
}
}
// 计算CRC校验值
uint32_t digest = 0;
if (dataSize > 0) {
digest = XXH32(buffer + headerSize, dataSize, 0);
}
// 检查CRC校验
bool valid = (digest == storedCRC);
if (!valid) {
MMKVWarning("file corrupted: %s, crc %u vs stored %u", m_path.c_str(), digest, storedCRC);
// 文件损坏,重置
m_actualSize = 0;
m_dirty = true;
} else {
// 校验通过,解析数据
if (m_dic) {
delete m_dic;
}
m_dic = new MMBuffer(dataSize);
if (dataSize > 0) {
memcpy(m_dic->getPtr(), buffer + headerSize, dataSize);
}
m_crcDigest = digest;
}
delete[] buffer;
return valid;
}
七、内存映射初始化
7.1 文件映射实现
mapFile方法会将文件映射到内存:
// MMKV.cpp
bool MMKV::mapFile() {
if (m_fd < 0 || m_fileLength <= 0) {
return false;
}
// 解除已有的映射
if (m_ptr) {
munmap(m_ptr, m_fileLength);
m_ptr = nullptr;
}
// 映射文件到内存
m_ptr = (unsigned char *) mmap(nullptr, m_fileLength, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
MMKVError("fail to mmap %s, %s", m_path.c_str(), strerror(errno));
m_ptr = nullptr;
return false;
}
// 创建输出缓冲区
if (m_output) {
delete m_output;
}
m_output = new CodedOutputData(m_ptr + Fixed32Size, m_fileLength - Fixed32Size);
return true;
}
7.2 输出缓冲区初始化
CodedOutputData负责处理数据的写入:
// CodedOutputData.cpp
CodedOutputData::CodedOutputData(unsigned char *ptr, size_t size)
: m_ptr(ptr)
, m_size(size)
, m_position(0)
, m_hasSpace(true) {}
// 写入不同类型数据的方法...
八、数据校验与恢复
8.1 CRC校验机制
MMKV使用CRC32算法进行数据校验:
// MMKV.cpp
void MMKV::updateCRCDigest(const unsigned char *ptr, size_t len, bool clear) {
if (clear) {
m_crcDigest = 0;
}
if (ptr && len > 0) {
m_crcDigest = XXH32(ptr, len, m_crcDigest);
}
}
bool MMKV::saveCRCDigest() {
int fd = open(m_crcPath.c_str(), O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);
if (fd < 0) {
MMKVError("fail to open: %s, %s", m_crcPath.c_str(), strerror(errno));
return false;
}
ssize_t result = write(fd, &m_crcDigest, Fixed32Size);
close(fd);
return (result == Fixed32Size);
}
8.2 文件损坏恢复
当检测到文件损坏时,MMKV会进行恢复操作:
// MMKV.cpp
bool MMKV::checkFile() {
// 读取当前CRC校验值
uint32_t storedCRC = 0;
{
int crcFd = open(m_crcPath.c_str(), O_RDONLY);
if (crcFd >= 0) {
read(crcFd, &storedCRC, Fixed32Size);
close(crcFd);
}
}
// 计算当前数据的CRC校验值
uint32_t digest = 0;
if (m_actualSize > 0 && m_ptr) {
digest = XXH32(m_ptr + Fixed32Size, m_actualSize, 0);
}
// 检查CRC校验
bool valid = (digest == storedCRC);
if (!valid) {
MMKVWarning("file corrupted: %s, crc %u vs stored %u", m_path.c_str(), digest, storedCRC);
// 文件损坏,尝试恢复
if (m_actualSize > 0 && m_ptr) {
// 尝试解析数据,尽可能恢复
MMBuffer data(m_actualSize);
memcpy(data.getPtr(), m_ptr + Fixed32Size, m_actualSize);
// 清空当前数据
m_actualSize = 0;
m_dirty = true;
// 重新写入有效数据
// ...恢复逻辑...
} else {
// 无法恢复,重置文件
m_actualSize = 0;
m_dirty = true;
}
// 更新CRC校验值
updateCRCDigest(nullptr, 0, true);
saveCRCDigest();
// 刷新到文件
flush(true);
}
return valid;
}
九、多进程支持初始化
9.1 进程间锁初始化
在多进程模式下,MMKV会初始化进程间锁:
// MMKV.cpp
// 在构造函数中初始化进程间锁
if (m_isInterProcess) {
m_sharedProcessLock = m_fileLock->createSharedLock();
m_exclusiveProcessLock = m_fileLock->createExclusiveLock();
}
9.2 多进程模式下的文件处理
多进程模式下,MMKV会采取额外的措施确保数据一致性:
// MMKV.cpp
// 在多进程模式下,每次读取前都检查文件是否有变化
bool MMKV::needLoadFromFile() {
if (!m_needLoadFromFile) {
return false;
}
SCOPED_LOCK(m_lock);
if (m_needLoadFromFile) {
// 检查文件是否有变化
struct stat st = {0};
if (fstat(m_fd, &st) == 0) {
size_t fileSize = (size_t) st.st_size;
if (fileSize != m_fileLength) {
// 文件大小变化,需要重新加载
m_needLoadFromFile = true;
}
}
if (m_needLoadFromFile) {
// 重新加载文件
loadFromFile();
m_needLoadFromFile = false;
}
}
return m_needLoadFromFile;
}
十、加密功能初始化
10.1 加密器初始化
如果提供了加密密钥,MMKV会初始化加密器:
// MMKV.cpp
// 在构造函数中初始化加密器
if (!cryptKey.empty()) {
m_crypter = new AESCrypter((const unsigned char *) cryptKey.data(), (int) cryptKey.length());
}
10.2 AES加密实现
AESCrypter类负责实现AES加密和解密:
// AESUtils.cpp
AESCrypter::AESCrypter(const unsigned char *key, int keyLen) {
if (key && keyLen > 0) {
// 初始化AES密钥
if (keyLen == 16 || keyLen == 24 || keyLen == 32) {
memcpy(m_key, key, keyLen);
m_keyLength = keyLen;
// 初始化加密和解密上下文
AES_set_encrypt_key(m_key, m_keyLength * 8, &m_encryptKey);
AES_set_decrypt_key(m_key, m_keyLength * 8, &m_decryptKey);
} else {
MMKVError("invalid AES key length: %d", keyLen);
}
}
}
// 加密方法
void AESCrypter::encrypt(unsigned char *iv, unsigned char *input, size_t length) {
if (!iv || !input || length == 0 || length % AES_BLOCK_SIZE != 0) {
return;
}
// 使用CBC模式加密
AES_cbc_encrypt(input, input, length, &m_encryptKey, iv, AES_ENCRYPT);
}
// 解密方法
void AESCrypter::decrypt(unsigned char *iv, unsigned char *input, size_t length) {
if (!iv || !input || length == 0 || length % AES_BLOCK_SIZE != 0) {
return;
}
// 使用CBC模式解密
AES_cbc_encrypt(input, input, length, &m_decryptKey, iv, AES_DECRYPT);
}
十一、常见问题与解决方案
11.1 初始化失败问题
问题描述:调用MMKV.initialize()后,获取实例失败。
可能原因:
- 存储目录不可写
- 权限不足
- 本地库加载失败
解决方案:
- 检查存储目录权限
- 确保正确调用了initialize()方法
- 检查本地库是否正确集成
11.2 多进程模式下的数据不一致问题
问题描述:在多进程模式下,不同进程间的数据不一致。
可能原因:
- 进程间锁使用不当
- 文件变化未及时检测
- 缓存未及时更新
解决方案:
- 确保在多进程模式下正确使用MMKV
- 增加文件变化检测频率
- 在适当的时候调用reload()方法刷新数据
11.3 加密相关问题
问题描述:启用加密后,数据无法正确读写。
可能原因:
- 加密密钥不正确
- 加密算法不兼容
- 数据损坏
解决方案:
- 确保加密密钥在所有进程中一致
- 检查加密算法版本
- 尝试清除并重新初始化加密数据