一、MMKV 是什么
MMKV 是微信团队开源的一款基于 mmap 内存映射的高性能 key-value 存储组件,底层序列化采用 Protobuf,支持加密、多进程共享、匿名内存等特性。它在微信内部从 2015 年使用至今,后移植到 Android、iOS、macOS、Windows、POSIX 及 HarmonyOS 平台。
与 Android 原生的 SharedPreferences 相比,MMKV 的核心优势在于:
| 维度 | SharedPreferences | MMKV |
|---|---|---|
| 写入方式 | 全量 XML 写入磁盘 | 增量 append 到 mmap 内存 |
| 同步机制 | apply()异步 /commit()同步阻塞 | 无需调用,写入即生效 |
| 序列化格式 | XML(文本,体积大) | Protobuf(二进制,体积小) |
| 崩溃安全 | 可能丢失未持久化数据 | mmap 由 OS 保证刷盘,crash 不丢 |
| 多进程 | 不支持 | 支持(文件锁机制) |
| 性能 | 写 1000 次 int 约 233ms | 写 1000 次 int 约 4ms |
二、核心架构分层
MMKV 的架构可分为三层:Java 层 → JNI 层 → Native 核心层,右侧通过 mmap 与文件系统交互。
1. Java 层
MMKV.java 实现了 SharedPreferences 和 SharedPreferences.Editor 接口,对外暴露 encode() / decode() 等方法。所有操作最终通过 nativeHandle(Native 层 MMKV 对象的地址)透传到 C++ 层。
// Java 层示例MMKVkv=MMKV.defaultMMKV();kv.encode("int",Integer.MIN_VALUE);intiValue=kv.decodeInt("int");2. JNI 层
mmkv_jni.cpp 负责 Java 与 C++ 的桥接,将 jstring 转为 std::string,再调用 MMKV::mmkvWithID() 获取或创建 Native 对象。
// JNI 方法签名extern"C"JNIEXPORT jlong JNICALLJava_com_tencent_mmkv_MMKV_getMMKVWithID(JNIEnv*env,jobject obj,jstring mmapID,jint mode,jstring cryptKey,jstring relativePath){// ... 转换为 std::string 后调用 MMKV::mmkvWithID()}3. Native 核心层(C++)
这是 MMKV 的核心,包含以下模块:
- MMKV 类:持有 m_dic(std::unordered_map),维护内存中的键值对。
- Protobuf 编解码:MiniPBCoder 负责将 m_dic 序列化为二进制,或从二进制反序列化。
- AES 加密:可选的 AESCrypt 模块,对数据进行 AES 加密。
- 进程锁:通过 flock() 或 fcntl() 实现跨进程文件锁,保证多进程安全。
- 内存管理:ensureMemorySize() 负责空间检查、扩容和内存重整。
三、核心机制详解
1. mmap 内存映射:为什么快?
mmap 是 Linux 提供的系统调用,将磁盘文件映射到进程的虚拟地址空间。此后对内存的读写就等价于对文件的读写,无需再经过 read() / write() 系统调用和内核态-用户态的数据拷贝。
// MMKV 中的 mmap 调用m_ptr=(char*)mmap(nullptr,m_size,PROT_READ|PROT_WRITE,MAP_SHARED,m_fd,0);关键点:
- PROT_READ | PROT_WRITE:映射区域可读可写。
- MAP_SHARED:修改会同步回写到文件,且对其他进程可见。
- 崩溃安全:数据写入 m_ptr 指向的内存后,即使 App 立即 crash,操作系统也会负责将"脏页"刷回磁盘,数据不会丢失。
2. 文件加载与校验:loadFromFile()
首次获取 MMKV 实例时,会调用 loadFromFile() 加载已有数据:
voidMMKV::loadFromFile(){// 1. 打开文件,获取大小m_fd=open(m_path.c_str(),O_RDWR|O_CREAT,S_IRWXU);// 2. 文件大小对齐到页大小整数倍(mmap 基于页机制)if(m_size<DEFAULT_MMAP_SIZE||(m_size%DEFAULT_MMAP_SIZE!=0)){m_size=((m_size/DEFAULT_MMAP_SIZE)+1)*DEFAULT_MMAP_SIZE;ftruncate(m_fd,m_size);// 扩容文件zeroFillFile(m_fd,oldSize,m_size-oldSize);// 补零}// 3. mmap 映射m_ptr=(char*)mmap(nullptr,m_size,PROT_READ|PROT_WRITE,MAP_SHARED,m_fd,0);// 4. 读取前 4 字节作为 actualSize(实际数据长度)memcpy(&m_actualSize,m_ptr,Fixed32Size);// 5. CRC 校验,失败则根据策略恢复或丢弃if(checkFileCRCValid()){// 6. 解密(如需要)→ Protobuf 反序列化 → 填充 m_dicMMBufferinputBuffer(m_ptr+Fixed32Size,m_actualSize,MMBufferNoCopy);if(m_crypter)decryptBuffer(*m_crypter,inputBuffer);MiniPBCoder::decodeMap(m_dic,inputBuffer);}// 7. 构造 CodedOutputData,指向可写入区域m_output=newCodedOutputData(m_ptr+Fixed32Size+m_actualSize,m_size-Fixed32Size-m_actualSize);}文件结构(前几个字节):
| 4 bytes: actualSize | 4 bytes: CRC32 | protobuf 数据 ... |3. 写入流程:增量追加(Append)
MMKV 最大的设计亮点是增量更新。修改一个已有 key 时,它不会修改文件中原有位置,而是直接在文件末尾 append 一条新记录。读取时只认最后一条。
boolMMKV::setDataForKey(MMBuffer&&data,conststd::string&key){SCOPEDLOCK(m_lock);SCOPEDLOCK(m_exclusiveProcessLock);checkLoadData();// 确保数据已加载// 更新内存中的 mapm_dic[key]=std::move(data);m_hasFullWriteback=false;returnappendDataWithKey(m_dic[key],key);// 追加到文件}appendDataWithKey() 的核心逻辑:
boolMMKV::appendDataWithKey(constMMBuffer&data,conststd::string&key){// 计算 key + value 在 protobuf 中占用的总字节数size_t size=keyLength+pbRawVarint32Size(keyLength);size+=data.length()+pbRawVarint32Size(data.length());// 确保空间足够(不够则触发内存重整)if(!ensureMemorySize(size))returnfalse;if(m_actualSize==0){// 空文件:全量写入整个 mapautoallData=MiniPBCoder::encodeDataWithObject(m_dic);writeAcutalSize(allData.length());m_output->writeRawData(allData);recaculateCRCDigest();}else{// 非空文件:增量追加writeAcutalSize(m_actualSize+size);m_output->writeString(key);m_output->writeData(data);// 加密 & 更新 CRCif(m_crypter)m_crypter->encrypt(ptr,ptr,size);updateCRCDigest(ptr,size,KeepSequence);}returntrue;}4. 内存重整:ensureMemorySize()
不断 append 会导致文件膨胀、冗余数据堆积。当剩余空间不足时,ensureMemorySize() 会触发内存重整(Memory Reclamation):
boolMMKV::ensureMemorySize(size_t newSize){if(newSize>=m_output->spaceLeft()){// 1. 将当前 m_dic 全量序列化MMBuffer data=MiniPBCoder::encodeDataWithObject(m_dic);size_t lenNeeded=data.length()+offset+newSize;// 2. 预测未来空间需求,按 2 倍扩容size_t futureUsage=avgItemSize*std::max<size_t>(8,(m_dic.size()+1)/2);if(lenNeeded+futureUsage>=m_size){do{m_size*=2;}while(lenNeeded+futureUsage>=m_size);ftruncate(m_fd,m_size);// 扩大文件zeroFillFile(m_fd,oldSize,m_size-oldSize);munmap(m_ptr,oldSize);// 解除旧映射m_ptr=(char*)mmap(m_ptr,m_size,...);// 重新映射}// 3. 全量回写(清理冗余)writeAcutalSize(data.length());deletem_output;m_output=newCodedOutputData(m_ptr+offset,m_size-offset);m_output->writeRawData(data);recaculateCRCDigest();m_hasFullWriteback=true;}returntrue;}扩容策略:文件大小按 2 倍 增长(类似 ArrayList),同时预测未来使用量,避免频繁扩容。
5. 多进程支持:文件锁
MMKV 通过文件锁实现多进程并发访问:
- 读-读:多个进程可同时持有读锁,并发读取。
- 读-写 / 写-写:通过互斥锁保证串行化。
锁的实现基于 flock() 或 fcntl(),在 loadFromFile() 和写入操作前后加锁/解锁。
四、与 SharedPreferences 的对比总结
| 特性 | SharedPreferences | MMKV |
|---|---|---|
| 存储格式 | XML 文本 | Protobuf 二进制 |
| 写入方式 | 全量写入磁盘 | 增量 append 到 mmap |
| 同步调用 | apply()/commit() | 无需调用,即时生效 |
| 崩溃安全 | apply()可能丢失 | mmap 由 OS 保证 |
| 多进程 | ❌ 不支持 | ✅ 支持 |
| 加密 | ❌ 不支持 | ✅ AES 可选 |
| 体积增量 | 无 | 约 50KB / ABI |
| 读取速度 | 解析 XML | 直接读内存 map |
五、使用注意事项
- 不要存储过大对象:MMKV 适合小数据高频读写,单文件过大(如 > 100MB)会影响重整性能。
- key 不宜过长:每个 key 都会随 value 一起序列化存储,长 key 会浪费空间。
- 多进程模式有开销:跨进程锁会带来一定性能损耗,非必要不开启。
- 注意内存重整时机:频繁修改大量 key 会频繁触发 ensureMemorySize(),导致全量序列化开销。
- 迁移成本:MMKV 提供了 MMKV.migrateFromSharedPreferences() 方法,可平滑迁移旧数据。
六、总结
MMKV = mmap(零拷贝内存映射)+ Protobuf(高效二进制序列化)+ 增量追加(避免全量写)+ 内存重整(自动清理冗余)+文件锁(多进程安全)
它用"操作内存"的方式替代了"操作文件",用"增量追加"替代了"全量覆盖",用"操作系统刷盘"替代了"手动同步",从而在性能、安全和易用性上全面超越了原生 SharedPreferences。