深入理解Android MMKV初始化流程:从Java到C++全解析(13)

282 阅读11分钟

深入理解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()后,获取实例失败。

可能原因

  1. 存储目录不可写
  2. 权限不足
  3. 本地库加载失败

解决方案

  1. 检查存储目录权限
  2. 确保正确调用了initialize()方法
  3. 检查本地库是否正确集成

11.2 多进程模式下的数据不一致问题

问题描述:在多进程模式下,不同进程间的数据不一致。

可能原因

  1. 进程间锁使用不当
  2. 文件变化未及时检测
  3. 缓存未及时更新

解决方案

  1. 确保在多进程模式下正确使用MMKV
  2. 增加文件变化检测频率
  3. 在适当的时候调用reload()方法刷新数据

11.3 加密相关问题

问题描述:启用加密后,数据无法正确读写。

可能原因

  1. 加密密钥不正确
  2. 加密算法不兼容
  3. 数据损坏

解决方案

  1. 确保加密密钥在所有进程中一致
  2. 检查加密算法版本
  3. 尝试清除并重新初始化加密数据