分析对象
- Core/:跨平台 C++ 核心(Android / iOS / macOS / HarmonyOS / Win32 共用)
- Android/MMKV/mmkv/src/main/java/:Android Java 壳层
- Android/MMKV/mmkv/src/main/cpp/native-bridge.cpp:JNI 桥接层
**源码版本:v1.3.16 https://github.com/Tencent/MMKV/releases#release-v1.3.16 写作原则:本文所有结论均可回溯到源码,函数与代码块后标注 文件:行号(1-based)。凡与流传甚广的「八股说法」不符之处,均显式标注 【纠偏】。
目录
1. 总体架构与分层
MMKV 不是一个「Android 库」,而是一个 带 Java/OC 薄壳的 C++ 库。理解源码的第一步,是明确职责边界:Java 层几乎不做任何存储逻辑,它只做三件事——加载 so、持有 native 指针、编解码参数。
#mermaid-svg-d2r8TYjVd03u6zxr{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-d2r8TYjVd03u6zxr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-d2r8TYjVd03u6zxr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-d2r8TYjVd03u6zxr .error-icon{fill:#552222;}#mermaid-svg-d2r8TYjVd03u6zxr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-d2r8TYjVd03u6zxr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-d2r8TYjVd03u6zxr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-d2r8TYjVd03u6zxr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-d2r8TYjVd03u6zxr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-d2r8TYjVd03u6zxr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-d2r8TYjVd03u6zxr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-d2r8TYjVd03u6zxr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-d2r8TYjVd03u6zxr .marker.cross{stroke:#333333;}#mermaid-svg-d2r8TYjVd03u6zxr svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-d2r8TYjVd03u6zxr p{margin:0;}#mermaid-svg-d2r8TYjVd03u6zxr .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-d2r8TYjVd03u6zxr .cluster-label text{fill:#333;}#mermaid-svg-d2r8TYjVd03u6zxr .cluster-label span{color:#333;}#mermaid-svg-d2r8TYjVd03u6zxr .cluster-label span p{background-color:transparent;}#mermaid-svg-d2r8TYjVd03u6zxr .label text,#mermaid-svg-d2r8TYjVd03u6zxr span{fill:#333;color:#333;}#mermaid-svg-d2r8TYjVd03u6zxr .node rect,#mermaid-svg-d2r8TYjVd03u6zxr .node circle,#mermaid-svg-d2r8TYjVd03u6zxr .node ellipse,#mermaid-svg-d2r8TYjVd03u6zxr .node polygon,#mermaid-svg-d2r8TYjVd03u6zxr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-d2r8TYjVd03u6zxr .rough-node .label text,#mermaid-svg-d2r8TYjVd03u6zxr .node .label text,#mermaid-svg-d2r8TYjVd03u6zxr .image-shape .label,#mermaid-svg-d2r8TYjVd03u6zxr .icon-shape .label{text-anchor:middle;}#mermaid-svg-d2r8TYjVd03u6zxr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-d2r8TYjVd03u6zxr .rough-node .label,#mermaid-svg-d2r8TYjVd03u6zxr .node .label,#mermaid-svg-d2r8TYjVd03u6zxr .image-shape .label,#mermaid-svg-d2r8TYjVd03u6zxr .icon-shape .label{text-align:center;}#mermaid-svg-d2r8TYjVd03u6zxr .node.clickable{cursor:pointer;}#mermaid-svg-d2r8TYjVd03u6zxr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-d2r8TYjVd03u6zxr .arrowheadPath{fill:#333333;}#mermaid-svg-d2r8TYjVd03u6zxr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-d2r8TYjVd03u6zxr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-d2r8TYjVd03u6zxr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-d2r8TYjVd03u6zxr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-d2r8TYjVd03u6zxr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-d2r8TYjVd03u6zxr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-d2r8TYjVd03u6zxr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-d2r8TYjVd03u6zxr .cluster text{fill:#333;}#mermaid-svg-d2r8TYjVd03u6zxr .cluster span{color:#333;}#mermaid-svg-d2r8TYjVd03u6zxr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-d2r8TYjVd03u6zxr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-d2r8TYjVd03u6zxr rect.text{fill:none;stroke-width:0;}#mermaid-svg-d2r8TYjVd03u6zxr .icon-shape,#mermaid-svg-d2r8TYjVd03u6zxr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-d2r8TYjVd03u6zxr .icon-shape p,#mermaid-svg-d2r8TYjVd03u6zxr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-d2r8TYjVd03u6zxr .icon-shape .label rect,#mermaid-svg-d2r8TYjVd03u6zxr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-d2r8TYjVd03u6zxr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-d2r8TYjVd03u6zxr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-d2r8TYjVd03u6zxr :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
nativeHandle (long = C++ 对象指针)
业务代码kv.encode(key, value)
Java 壳层MMKV.java
JNI 桥接native-bridge.cpp
C++ 门面MMKV.cpp
IO 与增量引擎MMKV_IO.cpp
内存映射文件MemoryFile.cpp
protobuf 编解码MiniPBCoder / CodedInOutData
跨进程锁InterProcessLock(.cpp/_Android.cpp)
AES-CFB128aes/AESCrypt.cpp
CRC32crc32/Checksum.h
磁盘文件mmap_id + mmap_id.crc
关键类与职责
| Java | Android/…/com/tencent/mmkv/MMKV.java | implements SharedPreferences, SharedPreferences.Editor(MMKV.java:52),native 方法声明集中在 MMKV.java:1642–1751 |
| JNI | Android/…/cpp/native-bridge.cpp | JNI_OnLoad 动态注册(native-bridge.cpp:59)、RegisterNatives 表(native-bridge.cpp:1101 起) |
| Core 门面 | Core/MMKV.cpp | 生命周期、set/get API、CRC 助手、实例字典 |
| Core IO | Core/MMKV_IO.cpp | 加载、增量写、多进程同步、扩容重整(全库最核心的 1900 行) |
| Core 文件 | Core/MemoryFile.cpp + _Android.cpp | fd 管理、mmap/munmap、ftruncate、msync、ashmem |
| Core 锁 | Core/InterProcessLock.cpp + _Android.cpp | 可递归的 flock/fcntl 封装 |
| Core 编码 | Core/MiniPBCoder.cpp、CodedInputData.cpp、CodedOutputData.cpp | 手写 protobuf wire-format 子集 |
| Core 元信息 | Core/MMKVMetaInfo.hpp | .crc 文件的内存结构(长度、CRC、sequence、IV、flags) |
一句话总纲:MMKV = mmap 提供的「可持久化内存」 + 「追加式 protobuf 记录流」 + 「内存哈希索引」 + 「.crc 元文件做校验与跨进程信令」 + 「flock 做互斥」。
2. 初始化链路:Java → JNI → C++
2.1 Java 侧:MMKV.initialize(context)
MMKV.java:91 是最常用的入口:
// Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKV.java:91
public static String initialize(@NonNull Context context) {
String rootDir = context.getFilesDir().getPath() + "/mmkv";
MMKVLogLevel logLevel = Build.DEBUG ? LevelDebug : LevelError;
return initialize(context, rootDir, null, logLevel, null);
}
真正干活的是全参重载 MMKV.java:192:
// MMKV.java:192(节选)
public static String initialize(@NonNull Context context, String rootDir, LibLoader loader,
MMKVLogLevel logLevel, MMKVHandler handler) {
...
if (isDebugBuild) {
enableProcessModeChecker(); // MMKV.java:199,仅 debug 包开启进程模式检查
}
...
String ret = doInitialize(rootDir, cacheDir, loader, logLevel, gWantLogReDirecting);
...
}
doInitialize(MMKV.java:216)负责 加载动态库 并调用 native:
// MMKV.java:216
private static String doInitialize(String rootDir, String cacheDir, LibLoader loader,
MMKVLogLevel logLevel, boolean wantLogReDirecting) {
if (loader != null) {
loader.loadLibrary("c++_shared");
loader.loadLibrary("mmkv");
} else {
System.loadLibrary("c++_shared");
System.loadLibrary("mmkv"); // MMKV.java:226
}
if (gInstance == null) {
gInstance = new MMKV(0);
jniInitialize(rootDir, cacheDir, logLevel.ordinal(), wantLogReDirecting);
}
return rootDir;
}
注意两点
- cacheDir 被传下去,用于 .tmp 目录(原子写文件时先写临时文件再 rename,见 Core/MemoryFile.cpp 的 copyFile)。
- 整个 Java 层用 gInstance 做单例,jniInitialize 只会真正执行一次。
2.2 JNI 侧:动态注册而非名字匹配
native-bridge.cpp:59 的 JNI_OnLoad 在 System.loadLibrary 时被系统回调,随后 registerNativeMethods(native-bridge.cpp:1158)用一张显式表把 Java native 方法绑定到 C 函数:
// Android/…/cpp/native-bridge.cpp:1101(表节选)
{"jniInitialize", "(Ljava/lang/String;Ljava/lang/String;IZ)V", (void *) mmkv::jniInitialize_2},
{"encodeBool", "(JLjava/lang/String;Z)Z", (void *) _encodeBool},
...
为什么用 RegisterNatives 而不去调 JNI_OnLoad?
jniInitialize_2(native-bridge.cpp:143)把路径转成 std::string 后调用 MMKV::initializeMMKV(…),并把 cacheDir 存进全局 g_android_tmpDir(native-bridge.cpp:153)。
2.3 Core 侧:一次性的 initialize()
// Core/MMKV.cpp:200
void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel, mmkv::LogHandler handler) {
SCOPED_LOCK(g_runtimeLock);
...
ThreadLock::ThreadOnce(&once_control, initialize); // 只执行一次
g_rootDir = rootDir;
mkPath(g_rootDir); // 递归建目录
}
被 ThreadOnce 保护的那个静态 initialize()(Core/MMKV.cpp:160)做了 性能关键的一步:运行时指令集探测:
// Core/MMKV.cpp:160
void initialize() {
g_instanceDic = new unordered_map<string, MMKV *>;
g_instanceLock = new ThreadLock();
g_instanceLock->initialize();
mmkv::DEFAULT_MMAP_SIZE = mmkv::getPageSize(); // :165 通常 = 4096
MMKVInfo("version %s, page size %d, arch %s", MMKV_VERSION, DEFAULT_MMAP_SIZE, MMKV_ABI);
#if defined(__aarch64__) && defined(__linux__)
auto hwcaps = getauxval(AT_HWCAP); // :170
if (hwcaps & HWCAP_AES) { // :172
openssl::AES_set_encrypt_key = openssl_aes_armv8_set_encrypt_key;
openssl::AES_encrypt = openssl_aes_armv8_encrypt;
...
}
if (hwcaps & HWCAP_CRC32) {
CRC32 = mmkv::armv8_crc32; // :184 硬件 CRC32
}
#endif
}
三个高频考点藏在里面:
- DEFAULT_MMAP_SIZE = getPageSize():「MMKV 按内存页(4KB)为粒度分配/扩展」这句话的源码出处。同时它也是「单文件最小映射大小」。
- CRC32 与 AES_* 是 函数指针,在 aarch64 上被替换成 ARMv8 硬件指令实现。所以「MMKV 的 CRC 校验居然不影响性能」的底气来自 CRC32 指令,而不是软件查表。
- 整个初始化用 ThreadOnce,多线程并发 initialize 安全。
3. 实例管理与缓存(不是 LRU)
3.1 mmkvWithID 的真实缓存结构
// Core/MMKV.cpp:60
unordered_map<string, MMKV *> *g_instanceDic;
// Core/MMKV.cpp:233
MMKV *MMKV::mmkvWithID(const string &mmapID, MMKVMode mode, string *cryptKey,
MMKVPath_t *rootPath, size_t expectedCapacity) {
...
SCOPED_LOCK(g_instanceLock); // :238
auto mmapKey = mmapedKVKey(mmapID, rootPath);
auto itr = g_instanceDic->find(mmapKey); // :241
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second;
return kv; // 命中直接复用
}
...
auto kv = new MMKV(mmapID, mode, cryptKey, rootPath, expectedCapacity);
(*g_instanceDic)[mmapKey] = kv; // :257
return kv;
}
【纠偏 · 重要】MMKV 的[ket-value]缓存 不是 LRU
网上称「MMKV 用 LRU 淘汰实例、自动 munmap 释放内存」。v1.3.16 源码中不存在任何 LRU 结构:
- 容器是裸的 unordered_map<string, MMKV *>(Core/MMKV.cpp:60),没有访问计数、没有淘汰队列;
- 实例一旦 new 出来就常驻,只有三处会 munmap:
- MMKV::onExit()(Core/MMKV.cpp:262)——遍历字典 sync() + clearMemoryCache() + delete;
- MMKV::clearMemoryCache(mmapID) 静态/实例版本(Core/MMKV.cpp:302);
- MemoryFile::truncate() 内部为重新映射而做的临时 munmap(Core/MemoryFile.cpp:173)。
真实结论:MMKV 的「内存占用」= 所有已创建实例的文件映射之和。因为 mmap 的是 MAP_SHARED 文件页,这些页是可回收的 page cache,内存紧张时内核可以直接换出而不丢数据,所以常驻映射 ≠ 常驻 RSS 压力。这才是 MMKV 不需要 LRU 的原因。
3.2 Java 侧的 nativeHandle
Java 层的 MMKV 对象只是一个 指针包装:
// MMKV.java:601
private static MMKV checkProcessMode(long handle, String mmapID, int mode) throws RuntimeException {
if (handle == 0) {
throw new RuntimeException("Fail to create an MMKV instance [" + mmapID + "] in JNI");
}
if (!isProcessModeCheckerEnabled) {
return new MMKV(handle);
}
synchronized (checkedHandleSet) {
if (!checkedHandleSet.contains(handle)) {
if (!checkProcessMode(handle)) { // 调 native 探测
throw new IllegalArgumentException("Opening a multi-process MMKV instance ["
+ mmapID + "] with SINGLE_PROCESS_MODE!");
}
checkedHandleSet.add(handle);
}
}
return new MMKV(handle);
}
- handle = C++ 侧 MMKV* 的 reinterpret_cast<int64_t>;
- 实例级 native 方法第一个参数一律是 long handle(MMKV.java:1642–1751 的 private static native 声明块);带 _2 后缀的重载多一个 int expireDurationInSecond,对应 C++ 的 set(value, key, expireDuration);
- Java 对象可以被 GC,C++ 实例不会——所以「MMKV 泄漏 Activity」的风险点在 Java 层:MMKV 对象不持有 Context(initialize 只用了一次 getFilesDir()),这是它天生不泄漏的原因,而不是「用了弱引用」。
【纠偏】「MMKV 用弱引用关联 Context」
MMKV.java 中 没有 WeakReference<Context>。它在 initialize(Context) 里只同步读取 context.getFilesDir() 和 getCacheDir() 两个字符串,之后与 Context 再无关系。正确说法是:MMKV 不长期持有 Context,因此不存在 Activity 泄漏路径。
3.3 MMKVMode 位定义
// Core/MMKV.h
enum MMKVMode {
MMKV_SINGLE_PROCESS = 1 << 0,
MMKV_MULTI_PROCESS = 1 << 1,
MMKV_CONTEXT_MODE_MULTI_PROCESS = 1 << 2, // Android 私有
MMKV_ASHMEM = 1 << 3, // 共享内存(匿名)模式
MMKV_BACKUP = 1 << 4, // 只读备份用
};
其中 MMKV_ASHMEM 只在 Android 有意义(Core/MMKV_Android.cpp:90 起的私有构造函数),用于 跨应用共享(配合 MMKVContentProvider 传 fd);MMKV_BACKUP 用于 backupTo() 时以只读方式加载。
4. mmap 与文件布局
4.1 一个 MMKV 实例 = 两个文件
// Core/MMKV.cpp:81(构造函数初始化列表,节选)
MMKV::MMKV(const string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath, size_t expectedCapacity)
: m_mmapID(mmapID)
, m_path(mappedKVPathWithID(m_mmapID, mode, rootPath)) // 数据文件
, m_crcPath(crcPathWithID(m_mmapID, mode, rootPath)) // 元文件:<id>.crc
, m_dic(nullptr)
, m_dicCrypt(nullptr)
, m_expectedCapacity(std::max<size_t>(DEFAULT_MMAP_SIZE,
roundUp<size_t>(expectedCapacity, DEFAULT_MMAP_SIZE))) // :87
, m_file(new MemoryFile(m_path, m_expectedCapacity)) // :88
, m_metaFile(new MemoryFile(m_crcPath)) // :89
, m_metaInfo(new MMKVMetaInfo()) // :90
, m_crypter(nullptr)
, m_lock(new ThreadLock()) // :92 进程内互斥锁
, m_fileLock(new FileLock(m_metaFile->getFd())) // :93 ★锁加在 .crc 的 fd 上
, m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType)) // :94
, m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))// :95
, m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0) { // :96
构造函数尾部两行非常重要,它们解释了一个常见疑问:
// Core/MMKV.cpp:117
m_sharedProcessLock->m_enable = m_isInterProcess;
m_exclusiveProcessLock->m_enable = m_isInterProcess; // :118
// Core/MMKV.cpp:120 sensitive zone
/*{
SCOPED_LOCK(m_sharedProcessLock);
loadFromFile();
}*/ // :124 ★被注释掉了
- 单进程模式下文件锁是空操作(m_enable == false),不产生任何 flock 系统调用,性能与纯内存 map 几乎一致;
- 构造函数不加载数据,loadFromFile() 被显式注释掉(Core/MMKV.cpp:121-124,Android 版同样注释在 Core/MMKV_Android.cpp:84-88、:133-137)→ MMKV 是 懒加载(lazy load):第一次 get/set 才 loadFromFile() 读 meta、校验 CRC、扫描记录、建字典。这是「创建 MMKV 实例几乎零成本」的源码依据。
【精度提示】懒的是「解码」,不是「mmap」。 构造函数初始化列表里的 m_file(new MemoryFile(m_path, m_expectedCapacity))(Core/MMKV.cpp:88)就已经 open + ftruncate + zeroFill + mmap 了:MemoryFile 构造函数 → reloadFromFile()(POSIX Core/MemoryFile.cpp:53-55 / Android Core/MemoryFile_Android.cpp:73-76)→ getFileSize → truncate(roundSize) → mmap()(Core/MemoryFile.cpp:195-205)。 被推迟的只是读懂文件内容这一步:m_actualSize = 0、m_output = nullptr(Core/MMKV.cpp:97-98)、m_dic 是空 map。所以准确表述是「建映射在构造期,解字典在首次访问」。
作者本人在源码里留了注释承认这个设计:
// Core/MMKV_IO.cpp:73-82 loadFromFile() 开头
if (!m_file->isFileValid()) {
m_file->reloadFromFile(m_expectedCapacity);
} else if (m_isInterProcess) {
// the file size may change by other process between instance creation and loadFromFile
// because we have lazy load // :77 ★官方注释:我们就是懒加载
auto actualFileSize = m_file->getActualFileSize();
if (actualFileSize != m_file->getFileSize()) {
m_file->reloadFromFile(m_expectedCapacity); // :80 构造期之后文件被别人添加内容 → 重新 mmap
}
}
即:因为构造期只 mmap 不读,从「创建实例」到「首次访问」这段时间里别的进程可能已经把文件扩容,所以 loadFromFile() 必须回头比对真实文件大小并按需重新映射 —— 这段防御代码的存在本身就是懒加载设计的证据。
懒加载的开关与消费点(一个 bool 串起全链路):
// 置位:构造函数(三处构造函数都一样)
m_needLoadFromFile = true; // Core/MMKV.cpp:111 / MMKV_Android.cpp:76, :125
// 消费:唯一读该标志的地方
void MMKV::checkLoadData() { // Core/MMKV_IO.cpp:305
if (m_needLoadFromFile) { // :306
SCOPED_LOCK(m_sharedProcessLock);
m_needLoadFromFile = false; // :309 ★只加载这一次
loadFromFile(); // :310
return;
}
if (!m_isInterProcess) { // :313 单进程:之后 checkLoadData 直接 return,零开销
return;
}
... // 多进程:比 sequence / crcDigest / fileSize,见第 8 章
}
// 复位:确认无需重载
m_needLoadFromFile = false; // Core/MMKV_IO.cpp:143(loadFromFile 结尾)
// 反向置位:主动丢弃内存字典,逼下次访问重新 load
void MMKV::clearMemoryCache(bool keepSpace) { // Core/MMKV.cpp:302
if (m_needLoadFromFile) { return; } // :304 本来就没加载过 → 什么都不用做
m_needLoadFromFile = true; // :308
checkLoadData() 被塞在每一个会碰数据的入口第一行,这就是「首次 get/set 才真正加载」的实现方式
| setDataForKey | Core/MMKV_IO.cpp:587 | ★所有 encode 的公共下游 |
| getRawDataForKey | Core/MMKV_IO.cpp:544 | ★所有 decode 的公共下游 |
| reKey / trim / clearAll | Core/MMKV_IO.cpp:1348 / :1418 / :1462 | 换密钥、缩容、清空 |
| containsKey / count / totalSize / actualSize | Core/MMKV.cpp:1052 / :1067 / :1083 / :1089 | 连元信息查询也要先保证已加载 |
| removeValueForKey / allKeys / removeValuesForKeys | Core/MMKV.cpp:1099 / :1108 / :1138 | 删除同样要先建好字典 |
| checkReSetCryptKey | Core/MMKV.cpp:372、:381、:389 | 换密钥前三处兜底加载 |
| checkContentChanged | Core/MMKV.cpp:291 | 手动触发一次同步(可用来预热) |
| enableAutoKeyExpire / disableAutoKeyExpire / getExpireTimeForKey | Core/MMKV_IO.cpp:1629 / :1697 / :1763 | TTL 相关接口 |
| getDataWithoutMTimeForKey | Core/MMKV_IO.cpp:1788 | 开了过期时的读路径(见 :566-569 分流) |
一句话面试版:构造函数只做「开文件 + 撑到页对齐大小 + MAP_SHARED 映射 + 建锁 + 建空字典」,loadFromFile() 在三个平台的构造函数里都被注释掉;真正解析靠 checkLoadData() 在首个 encode/decode 里按 m_needLoadFromFile 触发一次。所以 MMKV.mmkvWithID("x") 循环创建 100 个实例不卡,但第一次 decodeInt 会为「老用户的大文件」付出一次性代价 —— 想把这个代价挪出关键路径,就在启动后台线程提前调一次 checkContentChanged() 或任意 decode。
4.2 数据文件的物理布局
偏移 0 偏移 fileSize
┌──────────────┬──────────────────┬───────────────────────┬──────┐
│ 4B Fixed32 │ 4B ItemSizeHolder│ KV record #1 │ free │
│ actualSize │ (随机伪装长度) │ KV record #2 │ space│
│ (老式兼容) │ │ … │ │
└──────────────┴──────────────────┴───────────────────────┴──────┘
↑ ↑
oldStyleWrite m_output 从
ActualSize() 这里开始追加
每条 KV 记录是纯 protobuf wire-format 的 长度前缀对:
[varint32 keyLen][key bytes][varint32 valueLen][value bytes]
对应源码里反复出现的三个尺寸常量:
// Core/PBUtility.h
constexpr uint32_t Fixed32Size = pbFixed32Size(); // = 4
// Core/MMKV_IO.cpp:348
constexpr uint32_t ItemSizeHolderSize = 4;
Core/MMKV_IO.cpp:473 的 oldStyleWriteActualSize() 每次都会把 actualSize 以 memcpy 写进文件头 4 字节:
// Core/MMKV_IO.cpp:473
void MMKV::oldStyleWriteActualSize(size_t actualSize) {
...
memcpy(m_file->getMemory(), &actualSize, Fixed32Size);
}
这 4 字节存在理由是「降级兼容」:老版本 MMKV 只认文件头长度。checkDataValid()(Core/MMKV_IO.cpp:230)会用它做 downgrade/upgrade 检测,本文第 10 章展开。
真正的权威长度和 CRC 不在数据文件里,而在 .crc 元文件里。 这是 MMKV 数据安全设计的关键:数据文件是「可追加的裸区」,元文件是「一页纸的账本」,账本一次写完整页(4KB 原子性),所以天然抗半写。
4.3 .crc(meta)文件的内存结构
// Core/MMKVMetaInfo.hpp(节选,字段顺序即落盘顺序)
uint32_t m_crcDigest = 0; // :54 数据区的 CRC32
uint32_t m_version = MMKVVersionSequence; // :55
uint32_t m_sequence = 0; // full write-back count :56
uint8_t m_vector[AES_KEY_LEN] = {}; // :57 AES-CFB 的 IV
uint32_t m_actualSize = 0; // :58
// confirmed info: it's been synced to file :60
struct {
uint32_t lastActualSize = 0;
uint32_t lastCRCDigest = 0;
uint32_t _reserved[16] = {};
} m_lastConfirmedMetaInfo; // :65
uint64_t m_flags = 0; // :67 如 EnableKeyExipre
// Core/MMKVMetaInfo.hpp:81 热路径上的「快写」:只改两个字段
void writeCRCAndActualSizeOnly(void *ptr) const {
auto other = (MMKVMetaInfo *) ptr;
other->m_crcDigest = m_crcDigest;
other->m_actualSize = m_actualSize;
}
// Core/MMKVMetaInfo.hpp:94
static_assert(sizeof(MMKVMetaInfo) <= (4 * 1024), "MMKVMetaInfo lager than one pagesize");
static_assert 那一行是设计的灵魂约束:整个 meta 结构必须 ≤ 一个内存页。因为内核对「单页对齐写」在实践中可视为原子,于是 跨进程读 meta 不需要读到撕裂数据,也不需要额外的管道/信号量。
4.4 mmap() 本体
// Core/MemoryFile.cpp:195
bool MemoryFile::mmap() {
auto oldPtr = m_ptr;
m_ptr = (char *) ::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_diskFile.m_fd, 0);
if (m_ptr == MAP_FAILED) {
MMKVError("fail to mmap [%s], %s", m_diskFile.m_path.c_str(), strerror(errno));
m_ptr = nullptr;
return false;
}
MMKVInfo("mmap to address [%p], oldPtr [%p], [%s]", m_ptr, oldPtr, m_diskFile.m_path.c_str());
return true;
}
【纠偏 · 重要】MMKV 不是「写时复制(COW)」,也不存在「修改时创建新内存页」
上面的 MAP_SHARED 就是反证:MAP_SHARED 的映射页 直接就是 page cache 中的那个物理页,所有映射同一文件的进程看到同一份数据,写入立即对彼此可见,由内核异步回写。
- 如果用 MAP_PRIVATE 才是 COW —— 那写入不会落盘,进程一退出数据就没了,多进程也彻底失效。MMKV 绝不可能用 COW。
- 「写入崩溃导致数据不丢」的真正原因是:脏页在内核 page cache 里,进程挂掉不影响内核把页回写;而 崩溃瞬间只可能丢掉「还没写入 mmap 的字节」,write() 到映射区的 memcpy 是同步完成的。
正确表述:「写入 = 对用户态可见、内核托管的共享页做 memcpy,脏页由内核异步 writeback;因此既有内存速度,又有落盘语义」。
4.5 空间扩容:truncate 与「填零」
// Core/MemoryFile.cpp(truncate 主干,节选)
bool MemoryFile::truncate(size_t newSize) {
...
auto oldSize = m_size;
m_size = size;
// round up 到 page 整数倍 + ftruncate
if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
}
if (::ftruncate(m_diskFile.m_fd, static_cast<off_t>(m_size)) != 0) {
MMKVError("fail to truncate [%s] to size %zu, %s", m_diskFile.m_path.c_str(), m_size, strerror(errno));
m_size = oldSize;
return false;
}
if (m_size > oldSize) {
//扩容的空间填0
if (!zeroFillFile(m_diskFile.m_fd, oldSize, m_size – oldSize)) {
MMKVError("fail to zeroFile [%s] to size %zu, %s", m_diskFile.m_path.c_str(), m_size, strerror(errno));
m_size = oldSize;
// redo ftruncate to its previous size
int status = ::ftruncate(m_diskFile.m_fd, static_cast<off_t>(m_size));
if (status != 0) {
MMKVError("failed to truncate back [%s] to size %zu, %s", m_diskFile.m_path.c_str(), m_size, strerror(errno));
} else {
MMKVError("success to truncate [%s] back to size %zu", m_diskFile.m_path.c_str(), m_size);
MMKVError("after truncate, file size = %zu", getActualFileSize());
}
return false;
}
}
if (m_ptr) {
if (munmap(m_ptr, oldSize) != 0) {
MMKVError("fail to munmap [%s], %s", m_diskFile.m_path.c_str(), strerror(errno));
}
}
auto ret = mmap();
if (!ret) {
doCleanMemoryCache(true);
}
return ret;
}
mmkv::zeroFillFile(fd, offset, size)(Core/MemoryFile.cpp 尾部工具函数)每次写 4096 字节 0 把新区间「占住」。原因:ftruncate 在很多文件系统中,只是逻辑上调整了文件大小,属于“稀疏文件(Sparse File)”处理。如果仅仅 ftruncate,操作系统可能并没有真正为这块新扩展的区域分配物理磁盘块。如果在后续写入过程中,磁盘空间正好耗尽,原本 ftruncate 成功的操作会导致随后的 write 或内存映射访问触发 SIGBUS 信号,从而导致程序崩溃。通过 zeroFillFile 进行显式写入,相当于强制触发了文件系统的物理块分配。如果空间不足,这个过程会直接返回失败,MMKV 可以及时捕获并处理错误,而不是在后续关键的读写流程中崩溃。
4.6 reloadFromFile:另一进程扩容后如何跟上
// Core/MemoryFile.cpp:207
void MemoryFile::reloadFromFile(size_t expectedCapacity) {
...
if (!m_diskFile.open()) { ... } else {
FileLock fileLock(m_diskFile.m_fd);
InterProcessLock lock(&fileLock, SharedLockType);
SCOPED_LOCK(&lock); // :224 先拿共享锁读 size
mmkv::getFileSize(m_diskFile.m_fd, m_size); // :226
size_t expectedSize = std::max<size_t>(DEFAULT_MMAP_SIZE,
roundUp<size_t>(expectedCapacity, DEFAULT_MMAP_SIZE));
if (m_size < expectedSize || (m_size % DEFAULT_MMAP_SIZE != 0)) {
InterProcessLock exclusiveLock(&fileLock, ExclusiveLockType);
SCOPED_LOCK(&exclusiveLock); // :231 需要放大 → 升级为独占
size_t roundSize = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
roundSize = std::max<size_t>(expectedSize, roundSize);
truncate(roundSize); // :235
} else {
auto ret = mmap(); // :237 直接映射
if (!ret) { doCleanMemoryCache(true); }
}
}
}
这里出现了 shared → exclusive 的锁升级,也正是 FileLock 必须做成「可递归 + 能临时让出共享锁」的原因(见 8.2)。
4.7 msync:MMKV 到底什么时候落盘
// Core/MemoryFile.cpp:184
bool MemoryFile::msync(SyncFlag syncFlag) {
if (m_ptr) {
auto ret = ::msync(m_ptr, m_size, syncFlag ? MS_SYNC : MS_ASYNC);
if (ret == 0) { return true; }
MMKVError("fail to msync [%s], %s", ...);
}
return false;
}
// Core/MMKV.cpp:1169
void MMKV::sync(SyncFlag flag) {
...
m_file->msync(flag);
m_metaFile->msync(flag); // 元文件也要同步,否则重启后 meta 比数据旧
}
日常 encode() 不会调用 msync(除了重整路径 doFullWriteBack 会 sync(MMKV_SYNC))。落盘完全交给内核。MMKV.sync(Async) / MMKV.sync(Sync) 是给「应用即将退后台、想立刻刷盘」的可选保险。
5. 序列化层:MiniPBCoder / CodedInOutData / KeyValueHolder
5.1 「protobuf」到底用到了什么程度
MMKV 没有引入完整 protobuf 运行时,而是在 Core/protobuf 相关文件里手写了 wire-format 的一个子集:varint、fixed32、length-delimited。好处是零 .proto、零生成代码、体积极小;代价是它不是通用的 protobuf,而是「protobuf 编码格式的 KV 流」。
5.2 索引结构 MMKVMap
// Core/MMKVPredef.h:228
using MMKVMap = std::unordered_map<std::string, mmkv::KeyValueHolder, KeyHasher, KeyEqualer>;
// Core/MMKVPredef.h:229
using MMKVMapCrypt = std::unordered_map<std::string, mmkv::KeyValueHolderCrypt, KeyHasher, KeyEqualer>;
KeyHasher / KeyEqualer 带 is_transparent 标记,启用 C++14 异构查找:用一个 string_view/临时 buffer 去查 map,不会因为查询而构造 std::string。读取路径上少一次堆分配。
5.3 KeyValueHolder:内存里只存「坐标」,不存值
// Core/KeyValueHolder.h:30
#pragma pack(push, 1)
struct KeyValueHolder {
uint16_t computedKVSize; // internal use only :33 头部 varint 占的字节数
uint16_t keySize;
uint32_t valueSize;
uint32_t offset; // :36 相对数据区起点的偏移
MMBuffer toMMBuffer(const void *basePtr) const; // :41
};
KeyValueHolder.cpp 里 toMMBuffer() 的核心就一行算术:
return MMBuffer((void *) ((uint8_t *) basePtr + offset + computedKVSize), valueSize, MMBufferNoCopy);
这是 MMKV 读取快的本质:m_dic 是一张 key → (offset, size) 的表,读值 = 在 mmap 区内做一次 零拷贝视图构造,既不 memcpy 也不反序列化整表。
#pragma pack(1) + uint16_t 是为了把每个 holder 压到 12 字节。字典有 10 万条时,紧凑布局能省下数 MB 常驻内存。
5.4 加密模式的三态 holder(一个精巧设计)
// Core/KeyValueHolder.h:46
enum KeyValueHolderType : uint8_t {
KeyValueHolderType_Direct, // store value directly
KeyValueHolderType_Memory, // store value in the heap memory
KeyValueHolderType_Offset, // store value by offset
};
// Core/KeyValueHolder.h:53(union 三选一)
struct KeyValueHolderCrypt {
KeyValueHolderType type = KeyValueHolderType_Direct;
union {
struct { uint8_t pbKeyValueSize; uint16_t keySize; uint32_t valueSize;
uint32_t offset; AESCryptStatus cryptStatus; }; // :59 Offset 态
struct { uint8_t paddedSize; uint8_t paddedValue[1]; }; // :66 Direct 态:值内联在 map 里
struct { uint32_t memSize; void *memPtr; }; // :71 Memory 态:值在堆上
};
static constexpr size_t MediumBufferSize() { return 256; } // :81
static bool isValueStoredAsOffset(size_t valueSize) { return valueSize > MediumBufferSize(); } // :85
};
5.4.1 先把尺寸账算清楚:SmallBufferSize() == 27
// Core/KeyValueHolder.cpp:60-66 KeyValueHolderCrypt(MMBuffer&&)
if (data.type == MMBuffer::MMBufferType_Small) {
static_assert(SmallBufferSize() >= MMBuffer::SmallBufferSize(),
"KeyValueHolderCrypt can't hold MMBuffer"); // :62
type = KeyValueHolderType_Direct;
paddedSize = static_cast<uint8_t>(data.length());
memcpy(paddedValue, data.getPtr(), data.length()); // :66 内联,永不越界
}
MMBuffer 自己的 SSO 容量是 sizeof(MMBuffer) – offsetof(paddedBuffer) = 16(Android/Linux 64 位)、24(Apple 64 位)、10(32 位,见 Core/MMBuffer.h:62-63 注释)。27 ≥ 24 恒成立,所以任何「MMBuffer 存在栈上」的值都保证能塞进 holder,编译期就杜绝了 memcpy 越界。
5.4.2 两个阈值、两个决策点(不是「256 一个标准」)
| 是否走 Offset 态 | isValueStoredAsOffset(valueSize) → valueSize > 256 | Core/KeyValueHolder.h:85;调用点 Core/MMKV_IO.cpp:593, 620, 649, 849, 931、Core/CodedInputDataCrypt.cpp:255 | MediumBufferSize() = 256 |
| 非 Offset 时选 Direct 还是 Memory | length <= SmallBufferSize() | Core/KeyValueHolder.cpp:45、:61 | SmallBufferSize() = 27 |
三态的真实划分区间(valueSize 指加密记录里 value 那段的长度,含它自己的 varint 头,见 MMKV_IO.cpp:592-593 的 sizeNeededForData = pbRawVarint32Size(len) + len):
- 0 ~ 27 B → Direct 态:值内联在 unordered_map 节点里,无额外分配;
- 28 ~ 256 B → Memory 态:值挂在 void *memPtr;
- > 256 B → Offset 态:内存里只留 offset + keySize + valueSize + pbKeyValueSize + cryptStatus。
边界细节:> 是严格大于,所以恰好 256 字节走的是非 Offset 态;27 这一侧是 <=,含 27。
5.4.3 三态分别在哪儿被创建
| 首次加载/增量解码 CodedInputDataCrypt::readData | >256 → Offset;否则先乐观写 type = Direct(:268),再被 :269 的构造返回值整体覆盖 → 实际可能升级成 Memory | Core/CodedInputDataCrypt.cpp:255-272 |
| 写入已存在的 key(加密模式覆盖) | isValueStoredAsOffset 分叉:Offset → (keySize, valueSize, offset) + memcpy(&cryptStatus, &t_status, …);否则 → KeyValueHolderCrypt(std::move(data)) | Core/MMKV_IO.cpp:619-627 |
| 写入新 key | 同上分叉,走 emplace | Core/MMKV_IO.cpp:649-657 |
Core/KeyValueHolder.h:87-90 那三个构造函数的分工非常干净:(const void*, size_t) 只产 Direct 或 Memory;(MMBuffer&&) 只产 Direct 或 Memory;(keyLength, valueLength, offset) 只产 Offset(:85-89,初始化列表里写死 type(KeyValueHolderType_Offset))。
5.4.4 读路径的分水岭:toMMBuffer() —— 三态真正的收益不在省内存
// Core/KeyValueHolder.cpp:155-168
MMBuffer KeyValueHolderCrypt::toMMBuffer(const void *basePtr, const AESCrypt *crypter) const {
if (type == KeyValueHolderType_Direct) {
return MMBuffer((void *) paddedValue, paddedSize, MMBufferNoCopy); // :157
} else if (type == KeyValueHolderType_Memory) {
return MMBuffer(memPtr, memSize, MMBufferNoCopy); // :159
} else {
auto realPtr = (uint8_t *) basePtr + offset;
auto position = static_cast<uint32_t>(pbKeyValueSize + keySize);
auto realSize = position + valueSize; // :163
auto kvBuffer = MMBuffer(realPtr, realSize, MMBufferNoCopy);
auto decrypter = crypter->cloneWithStatus(cryptStatus); // :165 ★恢复解密现场
return decryptBuffer(decrypter, kvBuffer, position); // :166 ★现场解密
}
}
这是整段设计里最该讲出来的一点:Direct / Memory 态里存的是已经解密好的明文(basePtr/crypter 两个参数在 :157、:159 分支里压根没用到,调用方甚至直接传 nullptr, nullptr —— 见 Core/MMKV_IO.cpp:372)。也就是说:
- Direct / Memory 态 = 一次解密、终身命中。之后每次 decodeXxx 都是零拷贝返回内存视图,O(1),完全不做 AES 运算,也不碰 mmap 区;
- Offset 态 = 每次读都要现场解密:cloneWithStatus() 复制一个解密器并恢复到这条记录之前的 keystream 状态,再 decryptBuffer() 顺序跳过 position 字节(Core/KeyValueHolder.cpp:133-153 那个 16 字节步进循环)才能取到值。
所以三态的排序不是「越小越省内存」,而是「越小越倾向于用内存换 CPU」。配置项绝大多数是几字节到几十字节,直接以明文常驻,把流密码随机访问差的缺陷一次性绕掉。
5.4.5 为什么 Offset 态非要背 17 字节的 cryptStatus
AES-CFB128 是流式用法:第 n 个字节的 keystream 依赖前面所有块反馈出来的 m_vector(IV)与 m_number(块内游标),定义见 Core/aes/AESCrypt.h:48-51。光有 offset 是解不出值的 —— 必须知道「解到 offset 处为止,流走到哪一步了」。 cryptStatus 就是这个存档点。它在写入时由 thread_local AESCryptStatus t_status(Core/MMKV_IO.cpp:577)抓拍、memcpy 进 holder(:622、:653),于是任意一条 Offset 记录都能被独立、乱序地解密,不必从头扫文件。
5.4.6 重整时三态待遇不同:明文值是「影子副本」
// Core/MMKV_IO.cpp:366-374 prepareEncode(MMKVMapCrypt&)
for (auto &itr : dic) {
auto &kvHolder = itr.second;
if (kvHolder.type == KeyValueHolderType_Offset) {
totalSize += kvHolder.pbKeyValueSize + kvHolder.keySize + kvHolder.valueSize; // :369 留在原地待 memmove
smallestOffet = min(smallestOffet, kvHolder.offset);
} else {
vec.emplace_back(itr.first, kvHolder.toMMBuffer(nullptr, nullptr)); // :372 明文重新序列化
}
}
- memmoveDictionary 只把 Offset 态 收进 vec 参与搬移(Core/MMKV_IO.cpp:1171-1174),搬完重算 offset 并重新 getCurStatus(cryptStatus)(:1216-1225);
- Direct / Memory 态的明文则被 MiniPBCoder::encodeDataWithObject(vec) 重新编码,在 memmoveDictionary 尾部重新加密写到文件末尾(:1227-1239),而 holder 自身的 type 和内存里的明文纹丝不动。
结论:文件里那条 Direct/Memory 记录只是内存明文的影子副本,用来保证崩溃后还能重建;运行期读它一律走内存。toTuple()(Core/KeyValueHolder.h:102-104)只暴露 Offset 分支字段,正是这套分工的体现。
5.4.7 生命周期约束(union 手工管理 tagged union 的代价)
- 禁止拷贝:KeyValueHolderCrypt(const&) = delete、operator=(const&) = delete(Core/KeyValueHolder.h:107-108),因为 Memory 态持有裸 void*,拷一份就会 double free;
- 只允许 move:move()(Core/KeyValueHolder.cpp:91-112)里 Direct/Offset 是 POD 直接 memcpy 整块(:104-105),Memory 才转移指针并把源置空(:106-110);
- 析构按 tag 释放:if (type == KeyValueHolderType_Memory && memPtr) free(memPtr);(Core/KeyValueHolder.cpp:114-118);
- 长度取值必须分叉:realValueSize()(:120-130)三态分别返回 paddedSize / valueSize / memSize —— 直接读 valueSize 会在 Direct 态读到脏数据,这是读 union 最典型的坑。
5.4.8 与常见说法的差异(【纠偏】)
| 「核心判断标准是 MediumBufferSize() 256 字节」 | 256 只决定要不要 Offset 态;Direct 与 Memory 的分界是另一个常量 SmallBufferSize() = 27(Core/KeyValueHolder.cpp:45) |
| 「Memory 态把数据复制到堆上」 | 只对解码路径成立(Core/KeyValueHolder.cpp:52-56 确实 malloc + memcpy)。写入路径是零拷贝接管:KeyValueHolderCrypt(std::move(data)) 直接 memPtr = data.getPtr(); data.detach();(:80-81),复用 MMBuffer 已有的堆块;只有 Apple 的 NSData 才退回 memcpy(:71-78) |
| 「Direct 态是为了省一次指针跳转」 | 更主要的收益是免解密:toMMBuffer() 的 Direct 分支根本不使用 crypter(Core/KeyValueHolder.cpp:156-157),存的是明文 |
| 「三态是为了追求极致存储效率」 | 三态优化的对象是内存字典的常驻字节数 + 读放大,跟磁盘占用无关 —— 不管哪个态,文件里都实打实写着一份完整记录(MMKV_IO.cpp:372/:1227-1239)。省的是 RAM 和解密 CPU,不是磁盘 |
| 「非加密模式也这样」 | 三态只存在于加密模式。KeyValueHolder(Core/KeyValueHolder.h:32-42)永远只有 offset 一种形态(12 字节),因为不需解密时「只存坐标」本身就是零拷贝最优解,内联明文反而要额外维护两份副本 |
一句话面试版:加密模式下 holder 是个 29 字节的 tagged union,type 一字节决定剩下 28 字节怎么解读。> 256B 走 Offset 态,只存坐标加一份 17 字节的 AES 存档点,读时现场解密;≤ 27B 走 Direct 态把明文内联进 map 节点(蹭 union 尾部空间,零额外分配);中间那档走 Memory 态,写入时靠 detach() 白嫖 MMBuffer 的堆块。本质是用少量常驻明文,换掉流式 AES 无法随机访问的缺陷。
5.5 CodedOutputData:往 mmap 区「写指针推进」
// Core/CodedOutputData.h / .cpp
class CodedOutputData {
void writeData(const MMBuffer &data); // = writeRawVarint32(len) + memcpy
void writeRawData(const MMBuffer &data); // 只 memcpy,不带长度前缀
void seek(size_t extraLength); // 写指针 += extraLength
void setPosition(size_t position); // 写指针 = position
size_t spaceLeft();
void *curWritePointer();
};
越界保护是 抛异常:
// Core/CodedOutputData.cpp: writeRawByte
if (m_position > m_size) { throw std::out_of_range("out of bound"); }
MMKV_IO.cpp 的 append 路径外面套了 try/catch(…)(见 6.3),所以越界会被优雅地转成「写失败返回 false」,而不是崩溃。注意顺序:doAppendDataWithKey 先 ensureMemorySize(size) 再写,异常只是最后一道防线。
5.6 解码:decodeOneMap 与「删除也是追加」
// Core/MiniPBCoder.cpp:506
void MiniPBCoder::decodeOneMap(MMKVMap &dic, size_t position, bool greedy) {
auto block = [position, this](MMKVMap &dictionary) {
if (position) {
m_inputData->seek(position); // :509 增量解码:跳过已有前缀
} else {
m_inputData->readInt32(); // :511 全量解码:吃掉 ItemSizeHolder
}
while (!m_inputData->isAtEnd()) {
KeyValueHolder kvHolder;
const auto &key = m_inputData->readString(kvHolder);
if (key.length() > 0) {
m_inputData->readData(kvHolder);
if (kvHolder.valueSize > 0) {
dictionary[key] = std::move(kvHolder);
} else {
auto itr = dictionary.find(key);
if (itr != dictionary.end()) {
dictionary.erase(itr); // :523 ★valueSize==0 表示删除
}
}
}
}
};
if (greedy) {
try { block(dic); } catch (...) { ... } // :530 尽力恢复
} else {
try {
MMKVMap tmpDic;
block(tmpDic);
dic.swap(tmpDic); // :542 ★要么全成,要么全不动
} catch (...) { ... }
}
}
三个必考点:
6. 写路径全链路
以 kv.encode("name", true) 为例,完整调用链:
"MemoryFile.cpp"
"MMKV_IO.cpp"
"MMKV.cpp"
"native-bridge.cpp"
"MMKV.java"
"MemoryFile.cpp"
"MMKV_IO.cpp"
"MMKV.cpp"
"native-bridge.cpp"
"MMKV.java"
#mermaid-svg-h5DsdzQJGxstEDeS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-h5DsdzQJGxstEDeS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-h5DsdzQJGxstEDeS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-h5DsdzQJGxstEDeS .error-icon{fill:#552222;}#mermaid-svg-h5DsdzQJGxstEDeS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-h5DsdzQJGxstEDeS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-h5DsdzQJGxstEDeS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-h5DsdzQJGxstEDeS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-h5DsdzQJGxstEDeS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-h5DsdzQJGxstEDeS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-h5DsdzQJGxstEDeS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-h5DsdzQJGxstEDeS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-h5DsdzQJGxstEDeS .marker.cross{stroke:#333333;}#mermaid-svg-h5DsdzQJGxstEDeS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-h5DsdzQJGxstEDeS p{margin:0;}#mermaid-svg-h5DsdzQJGxstEDeS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h5DsdzQJGxstEDeS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-h5DsdzQJGxstEDeS .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h5DsdzQJGxstEDeS .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-h5DsdzQJGxstEDeS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-h5DsdzQJGxstEDeS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-h5DsdzQJGxstEDeS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-h5DsdzQJGxstEDeS .sequenceNumber{fill:white;}#mermaid-svg-h5DsdzQJGxstEDeS #sequencenumber{fill:#333;}#mermaid-svg-h5DsdzQJGxstEDeS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-h5DsdzQJGxstEDeS .messageText{fill:#333;stroke:none;}#mermaid-svg-h5DsdzQJGxstEDeS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h5DsdzQJGxstEDeS .labelText,#mermaid-svg-h5DsdzQJGxstEDeS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-h5DsdzQJGxstEDeS .loopText,#mermaid-svg-h5DsdzQJGxstEDeS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-h5DsdzQJGxstEDeS .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h5DsdzQJGxstEDeS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-h5DsdzQJGxstEDeS .noteText,#mermaid-svg-h5DsdzQJGxstEDeS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-h5DsdzQJGxstEDeS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h5DsdzQJGxstEDeS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h5DsdzQJGxstEDeS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h5DsdzQJGxstEDeS .actorPopupMenu{position:absolute;}#mermaid-svg-h5DsdzQJGxstEDeS .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-h5DsdzQJGxstEDeS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h5DsdzQJGxstEDeS .actor-man circle,#mermaid-svg-h5DsdzQJGxstEDeS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-h5DsdzQJGxstEDeS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
encodeBool(handle, key, value)
kv->>set((bool)value, key)
MMBuffer + CodedOutputData 编码到临时 buffer
setDataForKey(move(data), key)
SCOPED_LOCK(m_lock) + SCOPED_LOCK(m_exclusiveProcessLock)
checkLoadData() ← 多进程一致性入口
appendDataWithKey / overrideDataWithKey
ensureMemorySize(size)
(空间不足) truncate + doFullWriteBack
m_output->>writeData() 写进 mmap 区
m_actualSize += size
updateCRCDigest() → writeActualSize() 写 .crc
6.1 Java → C++ 的编解码
// MMKV.java:713
public boolean encode(String key, boolean value) {
return encodeBool(nativeHandle, key, value);
}
// native-bridge.cpp: _encodeBool(节选)
static jboolean _encodeBool(JNIEnv *env, jobject, jlong handle, jstring jsKey, jboolean value) {
auto kv = (MMKV *) handle;
...
return kv->set((bool) value, key);
}
// Core/MMKV.cpp:451
bool MMKV::set(bool value, MMKVKey_t key, uint32_t expireDuration) {
if (isKeyEmpty(key)) { return false; }
size_t size = mmkv_unlikely(m_enableKeyExpire) ? Fixed32Size + pbBoolSize() : pbBoolSize();
MMBuffer data(size);
CodedOutputData output(data.getPtr(), size);
output.writeBool(value);
if (mmkv_unlikely(m_enableKeyExpire)) {
auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
output.writeRawLittleEndian32(UInt32ToInt32(time)); // :461 值后面拼 4 字节过期时间
}
return setDataForKey(std::move(data), key);
}
要点:值先在 堆上的临时 MMBuffer 里编码好,再一次性写进 mmap 区。为什么不多写一步到位?因为需要先知道 size 才能 ensureMemorySize,且重整逻辑(checkSizeForOverride)需要 MMBuffer 做等值比较。mmkv_unlikely 是分支预测提示宏,把过期逻辑从热路径上「挤出去」。
6.2 setDataForKey:三岔路口
// Core/MMKV_IO.cpp:581(非加密分支,:662 起)
{
auto itr = m_dic->find(key);
if (itr != m_dic->end()) {
// ① compareBeforeSet:值没变就直接返回 true,一个字节都不写
if (isCompareBeforeSetEnabled()) {
auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
MMBuffer oldValueData = itr->second.toMMBuffer(basePtr);
...
if (oldValueData == data) {
return true; // :686 ★省掉一次 append + 一次 CRC + 一次 meta 写
}
}
// ② 全库只有一个 key 时,原地覆盖而不是追加
bool onlyOneKey = !m_isInterProcess && m_dic->size() == 1;
KVHolderRet_t ret;
if (onlyOneKey) {
ret = overrideDataWithKey(data, itr->second, isDataHolder); // :695
} else {
ret = appendDataWithKey(data, itr->second, isDataHolder); // :697
}
itr->second = std::move(ret.second);
} else {
// ③ 新 key:追加
ret = appendDataWithKey(data, key, isDataHolder);
m_dic->emplace(key, std::move(kvHolder));
}
}
入口的三件套(Core/MMKV_IO.cpp:585-587):
SCOPED_LOCK(m_lock); // :585 进程内递归互斥锁
SCOPED_LOCK(m_exclusiveProcessLock); // :586 跨进程独占文件锁
checkLoadData(); // :587 先同步其他进程的增量
onlyOneKey 这个特例值得单独说:微信里大量 MMKV 实例只存一个计数器。如果每次 +1 都追加,文件会无意义地膨胀并频繁重整。单 key 场景直接从头覆盖写,把「追加型存储」变成「原地更新」,代价是重整时不需要 memmove。它同时受 !m_isInterProcess 约束——多进程模式禁止 override,因为别的进程可能正在读那个区间。
6.3 追加写的正身:doAppendDataWithKey
// Core/MMKV_IO.cpp:822
KVHolderRet_t MMKV::doAppendDataWithKey(const MMBuffer &data, const MMBuffer &keyData,
bool isDataHolder, uint32_t originKeyLength) {
auto isKeyEncoded = (originKeyLength < keyData.length()); // :823
auto keyLength = static_cast<uint32_t>(keyData.length());
auto valueLength = static_cast<uint32_t>(data.length());
if (isDataHolder) { valueLength += pbRawVarint32Size(valueLength); }
size_t size = isKeyEncoded ? keyLength : (keyLength + pbRawVarint32Size(keyLength)); // :830
size += valueLength + pbRawVarint32Size(valueLength); // :832
SCOPED_LOCK(m_exclusiveProcessLock); // :834
bool hasEnoughSize = ensureMemorySize(size); // :836 ★空间保障
if (!hasEnoughSize || !isFileValid()) { return make_pair(false, KeyValueHolder()); }
try {
if (isKeyEncoded) { m_output->writeRawData(keyData); } // :856 已带 varint 头
else { m_output->writeData(keyData); } // :858
if (isDataHolder) { m_output->writeRawVarint32((int32_t) valueLength); }
m_output->writeData(data); // :863
} catch (std::exception &e) { MMKVError("%s", e.what()); return make_pair(false, KeyValueHolder()); }
catch (...) { MMKVError("append fail"); return make_pair(false, KeyValueHolder()); }
auto offset = static_cast<uint32_t>(m_actualSize);
auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;
if (m_crypter) { m_crypter->encrypt(ptr, ptr, size); } // :876 原地加密
m_actualSize += size; // :879
updateCRCDigest(ptr, size); // :880 ★增量 CRC + 写 meta
return make_pair(true, KeyValueHolder(originKeyLength, valueLength, offset));
}
性能解读(面试可直接背):一次 encode 的实际 CPU 成本 =
没有 write() 系统调用、没有 fsync、没有 全量序列化、没有 内核态/用户态拷贝。这就是「毫秒级甚至微秒级写入」的由来。
一个隐藏优化在 appendDataWithKey(data, kvHolder, …):
// Core/MMKV_IO.cpp:1001(key 已存在时,复用文件里已编码好的 key 字节)
uint32_t keyLength = kvHolder.keySize;
size_t rawKeySize = keyLength + pbRawVarint32Size(keyLength); // :1006
{ ... ensureMemorySize(size); } // :1015 ★先扩容,因为 offset 会变
auto basePtr = (uint8_t *) m_file->getMemory() + Fixed32Size;
MMBuffer keyData(basePtr + kvHolder.offset, rawKeySize, MMBufferNoCopy); // :1021
return doAppendDataWithKey(data, keyData, isDataHolder, keyLength);
更新已有 key 时,不再从 std::string 重新编码 key,而是直接从 mmap 里按 offset 切一段零拷贝视图复用(isKeyEncoded == true 分支)。省一次 varint 编码 + 一次拷贝。
6.4 更新后的「脏读」标记
setDataForKey 结尾会 m_hasFullWriteback = false。这个标志位是第 9 章 fullWriteback() 的短路开关:只要发生过 append,就认为文件里存在无效旧数据,需要择机重整。
6.5 删除、清空、改名
| removeValueForKey(key) | Core/MMKV.cpp → setDataForKey(MMBuffer(), key, true) | 追加一条 valueSize == 0 的墓碑,内存字典 erase |
| removeValuesForKeys(keys) | 循环调用,仅在最后 sync | 批量删除 |
| clearAll() / clearMemoryCache(keepSpace) | Core/MMKV_IO.cpp:1456 | writeActualSize(0,0,nullptr,IncreaseSequence);keepSpace=false 时 truncate 回默认大小 |
| reKey(newKey)(换加密密钥) | Core/MMKV_IO.cpp:1345 | 见 11.4 |
| trim() | Core/MMKV_IO.cpp:1413 | 唯一缩容入口,见 9.5 / 9.6 |
7. 读路径全链路
7.1 getBool 全文
// Core/MMKV.cpp:784
bool MMKV::getBool(MMKVKey_t key, bool defaultValue, bool *hasValue) {
if (isKeyEmpty(key)) { ... return defaultValue; }
SCOPED_LOCK(m_lock); // :791 进程内互斥
SCOPED_LOCK(m_sharedProcessLock); // :792 ★跨进程共享锁
auto data = getDataForKey(key);
if (data.length() > 0) {
try {
CodedInputData input(data.getPtr(), data.length());
if (hasValue != nullptr) { *hasValue = true; }
return input.readBool();
} catch (std::exception &exception) { MMKVError("%s", exception.what()); }
catch (...) { MMKVError("decode fail"); }
}
if (hasValue != nullptr) { *hasValue = false; }
return defaultValue;
}
// Core/MMKV_IO.cpp:543
MMBuffer MMKV::getRawDataForKey(MMKVKey_t key) {
checkLoadData(); // :544 ★每次读都做一次「有没有别人改过」的判断
auto itr = m_dic->find(key);
if (itr != m_dic->end()) {
auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
return itr->second.toMMBuffer(basePtr); // :558 零拷贝视图
}
MMBuffer nan;
return nan;
}
7.2 读成本分析
单进程模式下 m_sharedProcessLock->m_enable == false,锁是空操作,所以一次读的代价就是:
与 SharedPreferences 的根本差别:SP 的 getXxx 只是查 ArrayMap<String, Object>,看似也很快,但 它要求启动时把整个 XML 全量解析进内存,首次 getInt() 在 loadFromDisk 未完成时会阻塞;MMKV 是 懒加载 + 单值解码,且加载本身就是顺序扫一遍 mmap 区建 offset 表,无对象分配(除 map 节点)。
7.3 读用 shared、写用 exclusive 的意义
// 读:Core/MMKV.cpp:792 → SCOPED_LOCK(m_sharedProcessLock)
// 写:Core/MMKV_IO.cpp:586 → SCOPED_LOCK(m_exclusiveProcessLock)
多进程场景下,同一台机器上的 N 个进程可以并发读同一个 MMKV(flock 的 LOCK_SH 可重入叠加),但任何写入/重整都必须 LOCK_EX。这比「全局一把互斥锁」吞吐高一个量级,同时防止了在 memmove 重整期间读到半个对象。
7.4 懒加载:loadFromFile 主干
// Core/MMKV_IO.cpp:64
void MMKV::loadFromFile() {
loadMetaInfoAndCheck(); // :65 读 .crc,处理版本残留
if (m_crypter && m_metaInfo->m_version >= MMKVVersionRandomIV) {
m_crypter->resetIV(m_metaInfo->m_vector, sizeof(m_metaInfo->m_vector)); // :69
}
if (!m_file->isFileValid()) {
m_file->reloadFromFile(m_expectedCapacity); // :74 真正 mmap
} else if (m_isInterProcess) {
auto actualFileSize = m_file->getActualFileSize(); // :78
if (actualFileSize != m_file->getFileSize()) { // :79 别的进程扩过容
m_file->reloadFromFile(m_expectedCapacity);
}
}
if (!m_file->isFileValid()) {
MMKVError("file [%s] not valid", m_path.c_str());
} else {
bool loadFromFile = false, needFullWriteback = false;
checkDataValid(loadFromFile, needFullWriteback); // :88 ★CRC + 自愈决策
auto ptr = (uint8_t *) m_file->getMemory();
if (loadFromFile && m_actualSize > 0) {
MMBuffer inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy); // :97
clearDictionary(m_dic);
if (needFullWriteback) {
MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer); // :110 贪心抢救
} else {
MiniPBCoder::decodeMap(*m_dic, inputBuffer); // :119 正常全量
}
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() – Fixed32Size);
m_output->seek(m_actualSize); // :123 ★写指针对齐到末尾
if (needFullWriteback) { fullWriteback(); } // :125
} else {
SCOPED_LOCK(m_exclusiveProcessLock);
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() – Fixed32Size);
if (m_actualSize > 0) {
writeActualSize(0, 0, nullptr, IncreaseSequence); // :133 判定不可信 → 丢弃
sync(MMKV_SYNC);
} else {
writeActualSize(0, 0, nullptr, KeepSequence);
}
}
}
m_needLoadFromFile = false; // :143
}
这段代码浓缩了 MMKV 的「启动即体检」流程:读账本 → 映射文件 → 校验长度与 CRC → 决定「正常加载 / 抢救式加载 / 清空重来」→ 把写指针摆到有效数据末尾。
8. 多进程一致性与四层锁模型
8.1 一致性协议:checkLoadData()
这是 MMKV 多进程的 心脏,每一次读、每一次写之前都会执行:
// Core/MMKV_IO.cpp:305
void MMKV::checkLoadData() {
if (m_needLoadFromFile) { // ① 本机首次使用 → 全量加载
SCOPED_LOCK(m_sharedProcessLock);
m_needLoadFromFile = false;
loadFromFile();
return;
}
if (!m_isInterProcess) {
return; // ② ★单进程直接返回,零开销
}
if (!m_metaFile->isFileValid()) { return; }
SCOPED_LOCK(m_sharedProcessLock);
MMKVMetaInfo metaInfo; // ③ 用栈上的临时副本读账本
metaInfo.read(m_metaFile->getMemory()); // :323
if (m_metaInfo->m_sequence != metaInfo.m_sequence) {
// ④ sequence 变了 = 别的进程做过「全量重整 / clearAll / reKey」
MMKVInfo("[%s] oldSeq %u, newSeq %u", m_mmapID.c_str(), m_metaInfo->m_sequence, metaInfo.m_sequence);
SCOPED_LOCK(m_sharedProcessLock);
clearMemoryCache();
loadFromFile(); // :329 只能整体重载
notifyContentChanged();
} else if (m_metaInfo->m_crcDigest != metaInfo.m_crcDigest) {
// ⑤ crcDigest 变了 = 数据区被追加过
SCOPED_LOCK(m_sharedProcessLock);
size_t fileSize = m_file->getActualFileSize();
if (m_file->getFileSize() != fileSize) {
// ⑥ 文件大小也变了 = 别的进程扩容了 → 映射区已失效,只能全量重载
MMKVInfo("file size has changed [%s] from %zu to %zu", ...);
clearMemoryCache();
loadFromFile();
} else {
partialLoadFromFile(); // :342 ★增量同步(最优路径)
}
notifyContentChanged();
}
}
这段代码回答了「写指针同步」这道八股的真实形态。 流传的说法是「每个进程缓存写指针,写入时同步更新 mmap 内存中的全局指针」——不完全对。真实机制是:
- 写指针的「广播介质」不是数据文件里的一个全局变量,而是 .crc 元文件里的 (m_actualSize, m_crcDigest, m_sequence) 三元组;
- 写方每次 append 后通过 updateCRCDigest() → writeActualSize() 把新的 actualSize/crcDigest 落到 meta 页(Core/MMKV.cpp:436、Core/MMKV_IO.cpp:487);
- 读方不需要任何 IPC/通知,只要 memcmp 自己缓存的 m_crcDigest 和 meta 页里的值(metaInfo.read() 就是一次 memcpy 40 字节,Core/MMKVMetaInfo.hpp:88)。
三种变更的区分逻辑(这是真正的设计难点,也是面试的加分点):
| 别人 append 了新数据 | sequence 不变、crcDigest 变、fileSize 不变 | partialLoadFromFile() 增量 |
| 别人做了 fullWriteback(重整) | sequence 变(IncreaseSequence) | clearMemoryCache() + loadFromFile() |
| 别人扩容了文件 | crcDigest 变 且 getFileSize() != getActualFileSize() | 全量重载(老映射区已覆盖不了新文件) |
为什么重整必须 sequence++?因为重整之后 所有 offset 全变了,但 crcDigest 有可能恰好和旧值相同(数据内容一样只是位置变了),单靠 digest 无法感知。m_sequence 就是为此存在的「代数(generation)」。注释写得很直白:uint32_t m_sequence = 0; // full write-back count(Core/MMKVMetaInfo.hpp:56)。
8.2 增量同步:partialLoadFromFile()
// Core/MMKV_IO.cpp:147
void MMKV::partialLoadFromFile() {
if (!m_file->isFileValid()) { return; }
m_metaInfo->read(m_metaFile->getMemory());
size_t oldActualSize = m_actualSize;
m_actualSize = readActualSize(); // :154 从 meta 拿权威长度
auto fileSize = m_file->getFileSize();
if (m_actualSize > 0) {
if (m_actualSize < fileSize && m_actualSize + Fixed32Size <= fileSize) {
if (m_actualSize > oldActualSize) { // :161 只处理「增长」
auto position = oldActualSize;
size_t addedSize = m_actualSize – position;
auto basePtr = (uint8_t *) m_file->getMemory() + Fixed32Size;
// incremental update crc digest
m_crcDigest = (uint32_t) CRC32(m_crcDigest, basePtr + position, (z_size_t) addedSize); // :166 ★
if (m_crcDigest == m_metaInfo->m_crcDigest) {
MMBuffer inputBuffer(basePtr, m_actualSize, MMBufferNoCopy);
MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer, position); // :175 只解 [position, end)
m_output->seek(addedSize); // :177 写指针跟上去
m_hasFullWriteback = false;
return;
} else {
MMKVError("m_crcDigest[%u] != m_metaInfo->m_crcDigest[%u]", ...); // :184
}
}
}
}
// something is wrong, do a full load
clearMemoryCache();
loadFromFile(); // :190 ★兜底降级
}
增量同步的三步走:
任何一步不满足(长度不合法、CRC 不匹配),立刻降级为 clearMemoryCache() + loadFromFile() 全量重来。「增量优化 + 全量兜底」是 MMKV 所有快路径的统一范式。
8.3 文件锁:flock 而非 fcntl/信号量
// Core/InterProcessLock.cpp:75
static int32_t LockType2FlockType(LockType lockType) {
switch (lockType) {
case SharedLockType: return LOCK_SH;
case ExclusiveLockType: return LOCK_EX;
}
return LOCK_EX;
}
// Core/InterProcessLock.cpp:85
bool FileLock::platformLock(LockType lockType, bool wait, bool unLockFirstIfNeeded, bool *tryAgain) {
#ifdef MMKV_ANDROID
if (m_isAshmem) { return ashmemLock(lockType, wait, unLockFirstIfNeeded, tryAgain); } // :87
#endif
auto realLockType = LockType2FlockType(lockType);
auto cmd = wait ? realLockType : (realLockType | LOCK_NB); // :92 非阻塞加 LOCK_NB
if (unLockFirstIfNeeded) {
auto ret = flock(m_fd, realLockType | LOCK_NB); // :95 先试一把
if (ret == 0) { return true; }
// let's be gentleman: unlock my shared-lock to prevent deadlock
ret = flock(m_fd, LOCK_UN); // :100 ★主动让出共享锁
}
auto ret = flock(m_fd, cmd); // :106
if (ret != 0) {
if (tryAgain) { *tryAgain = (errno == EWOULDBLOCK); } // :109
// try recover my shared-lock
if (unLockFirstIfNeeded) {
ret = flock(m_fd, LockType2FlockType(SharedLockType)); // :116 ★失败再还原
}
return false;
}
return true;
}
为什么选 flock?
- flock 锁在 打开文件描述符(open file description) 上,不是 fcntl 那种锁在 (进程, inode) 上。含义:进程死亡时,内核关闭其所有 fd,锁自动释放。MMKV 因此在注释和文档里敢称 robust —— 不会因为某个进程被 kill -9 而留下永久死锁。
- fcntl 记录锁在同一进程内不同 fd 之间会互相「意外释放」,且与 NFS 语义纠缠,POSIX 有名坑。
- flock 天然支持 LOCK_SH(多读)/LOCK_EX(单写)语义,正好匹配 MMKV 的读写锁模型。
唯一的例外是 ashmem:Android 共享内存 fd 不支持 flock,所以走 fcntl(F_SETLKW/F_SETLK) 的 struct flock 记录锁(Core/InterProcessLock_Android.cpp:47 的 ashmemLock)。为此 Android 构造函数还要专门「强制使用 fcntl」以免和 MemoryFile::reloadFromFile() 的 flock 冲突(Core/MMKV_Android.cpp:90-137 附近注释)。
8.4 可递归 + 锁升级:FileLock 的「君子协议」
POSIX flock 本身对同一 fd 重复加锁不递归(再加一把 EX 会自锁死)。MMKV 因此在包装层做了引用计数:
// Core/InterProcessLock.h
class FileLock {
int m_sharedLockCount;
int m_exclusiveLockCount;
...
};
规则(Core/InterProcessLock.cpp 的 doLock):
- 已持有 shared,再要 shared → 不打破已有锁,计数 ++;
- 已持有 exclusive,再要 exclusive → 计数 ++(同进程递归);
- 已持有 shared,却要 exclusive → unLockFirstIfNeeded = true,先 LOCK_UN 让出共享锁(:100 “let’s be gentleman”),再试 LOCK_EX | LOCK_NB;拿不到就把共享锁 还原(:116)。
unlock 同理:释放独占后如果 m_sharedLockCount > 0,会 flock(LOCK_SH) 退回共享态(Core/InterProcessLock.cpp:134 的 platformUnLock(bool unlockToSharedLock))。
这正是 4.6 中 reloadFromFile 能做「shared → exclusive 升级」的底层支撑。
8.5 四层锁全景
| ① 全局实例表 | g_instanceLock(Core/MMKV.cpp:238) | SCOPED_LOCK | 创建/查找/销毁实例 |
| ② 进程内 | m_lock(ThreadLock,pthread_mutex 递归锁) | 每个 MMKV 一把 | 所有 set/get/trim/reKey |
| ③ 跨进程 | m_fileLock + m_sharedProcessLock/m_exclusiveProcessLock | 每个 MMKV 一把,加在 .crc 的 fd 上 | 仅 MULTI_PROCESS_MODE |
| ④ 进程模式检查 | m_fileModeLock(Android,加在 数据文件 fd 上) | 检测用 | 仅 debug 构建 |
【纠偏】③ 的锁为什么加在 .crc 而不是数据文件? 见 Core/MMKV.cpp:93:m_fileLock(new FileLock(m_metaFile->getFd()))。原因是数据文件会被 truncate + munmap/mmap 反复重建映射,而 meta 文件从头到尾只有一个稳定的 fd;把锁钉在「永不重生的那一个 fd」上,锁的语义才连续。
也正因为如此,第 ④ 层探测锁刻意用数据文件的 fd(Core/MMKV_Android.cpp 中 m_fileModeLock = new FileLock(m_file->getFd(), true /*isAshmem*/) 的同类设计),两把锁不能是同一个 fd,否则自相干扰。
8.6 进程模式误用检测(checkProcessMode)
一个非常现实的线上问题:同一个 mmapID,A 处用 MULTI_PROCESS_MODE、B 处用 SINGLE_PROCESS_MODE 打开,数据就会错乱。MMKV 的解法:
// Core/MMKV_Android.cpp:222
bool MMKV::checkProcessMode() {
if (!m_file->isFileValid()) { return true; }
if (m_isInterProcess) {
if (!m_exclusiveProcessModeLock) {
m_exclusiveProcessModeLock = new InterProcessLock(m_fileModeLock, ExclusiveLockType);
}
auto tryAgain = false;
auto exclusiveLocked = m_exclusiveProcessModeLock->try_lock(&tryAgain); // :234
if (exclusiveLocked) { return true; } // 能独占 → 没人用
auto shareLocked = m_sharedProcessModeLock->try_lock(); // :238
if (!shareLocked) { m_exclusiveProcessModeLock->try_lock(); return true; }
else {
if (!tryAgain) {
exclusiveLocked = m_exclusiveProcessModeLock->try_lock(&tryAgain); // :246 再试一次排除 OS 抖动
if (!exclusiveLocked && !tryAgain) { exclusiveLocked = true; } // 仍异常 → 放过
}
...
}
}
...
}
判定逻辑:想独占却拿不到,但共享锁能拿到 → 说明 已经有别人以 SINGLE_PROCESS_MODE 打开了这个实例(单进程实例会持有一把 shared 模式锁)。此时 Java 层 checkProcessMode(handle, mmapID, mode)(MMKV.java:601)抛出 IllegalArgumentException。
- 只在 debug 构建 自动开启:MMKV.java:199 的 if (isDebugBuild) { enableProcessModeChecker(); };release 可用 MMKV.enableProcessModeChecker() 手动打开。
- checkedHandleSet(MMKV.java:57)保证同一个 handle 只检测一次,避免性能损耗。
9. 空间管理:扩容、重整与收缩
9.1 触发点:ensureMemorySize
// Core/MMKV_IO.cpp:402
bool MMKV::ensureMemorySize(size_t newSize) {
if (!isFileValid()) { MMKVWarning("[%s] file not valid", m_mmapID.c_str()); return false; }
if (newSize >= m_output->spaceLeft() // 空间不够
|| (m_crypter ? m_dicCrypt->empty() : m_dic->empty())) { // 或首次写入
if (m_enableKeyExpire) { filterExpiredKeys(); } // :411 先清过期 key
auto preparedData = m_crypter ? prepareEncode(*m_dicCrypt) : prepareEncode(*m_dic);
return expandAndWriteBack(newSize, std::move(preparedData),
m_crypter ? !m_dicCrypt->empty() : !m_dic->empty()); // :416
}
return true;
}
prepareEncode(Core/MMKV_IO.cpp:350)是「只算长度,不做序列化」的经典优化:
static pair<MMBuffer, size_t> prepareEncode(const MMKVMap &dic) {
size_t totalSize = ItemSizeHolderSize; // :352 给占位头留出位置
for (auto &itr : dic) {
auto &kvHolder = itr.second;
totalSize += kvHolder.computedKVSize + kvHolder.valueSize; // :355 ★holder 里存了尺寸
}
return make_pair(MMBuffer(), totalSize);
}
KeyValueHolder.computedKVSize 在建索引时就算好了「varint(keyLen) + keyLen + varint(valueLen)」,所以算总长 不需要遍历真实字节,整个重整的尺寸预估是纯 O(dic.size()) 的整数加法。
9.2 扩容策略:翻倍 + 预留未来用量
// Core/MMKV_IO.cpp:422
bool MMKV::expandAndWriteBack(size_t newSize, std::pair<mmkv::MMBuffer, size_t> preparedData, bool needSync) {
auto fileSize = m_file->getFileSize();
auto sizeOfDic = preparedData.second;
size_t lenNeeded = sizeOfDic + Fixed32Size + newSize; // :425
size_t nowDicCount = m_crypter ? m_dicCrypt->size() : m_dic->size();
size_t laterDicCount = std::max<size_t>(1, nowDicCount + 1);
size_t avgItemSize = (lenNeeded + laterDicCount – 1) / laterDicCount; // :429 向上取整平均
size_t futureUsage = avgItemSize * std::max<size_t>(8, laterDicCount / 2); // :430
if (lenNeeded >= fileSize || (needSync && (lenNeeded + futureUsage) >= fileSize)) {
size_t oldSize = fileSize;
do { fileSize *= 2; } while (lenNeeded + futureUsage >= fileSize); // :435 指数扩容
MMKVInfo("extending [%s] file size from %zu to %zu, incoming size:%zu, future usage:%zu", ...);
// if we can't extend size, rollback to old state
if (!m_file->truncate(fileSize)) { return false; } // :443 ★失败即放弃,不动老数据
if (!isFileValid()) { return false; } // :448
}
return doFullWriteBack(std::move(preparedData), nullptr, needSync); // :453
}
两处细节值得在面试中主动点出来:
9.3 重整的精髓:memmoveDictionary —— 不重新序列化,只搬字节
// Core/MMKV_IO.cpp:1102
// we don't need to really serialize the dictionary, just reuse what's already in the file
static void memmoveDictionary(MMKVMap &dic, CodedOutputData *output, uint8_t *ptr,
AESCrypt *encrypter, size_t totalSize) {
auto originOutputPtr = output->curWritePointer();
auto writePtr = originOutputPtr + ItemSizeHolderSize; // :1107 跳过占位头
if (!dic.empty()) {
// sort by offset
vector<KeyValueHolder *> vec;
vec.reserve(dic.size());
for (auto &itr : dic) { vec.push_back(&itr.second); }
sort(vec.begin(), vec.end(),
[](const auto &l, const auto &r) { return l->offset < r->offset; }); // :1116
// merge nearby items to make memmove quicker
vector<pair<uint32_t, uint32_t>> dataSections; // pair(offset, size)
dataSections.emplace_back(vec.front()->offset, vec.front()->computedKVSize + vec.front()->valueSize);
for (size_t index = 1, total = vec.size(); index < total; index++) {
auto kvHolder = vec[index];
auto &lastSection = dataSections.back();
if (kvHolder->offset == lastSection.first + lastSection.second) {
lastSection.second += kvHolder->computedKVSize + kvHolder->valueSize; // :1125 相邻合并
} else {
dataSections.emplace_back(kvHolder->offset, kvHolder->computedKVSize + kvHolder->valueSize);
}
}
// do the move
auto basePtr = ptr + Fixed32Size;
for (auto §ion : dataSections) {
memmove(writePtr, basePtr + section.first, section.second); // :1134 ★整段搬迁
writePtr += section.second;
}
// update offset
if (!encrypter) {
auto offset = ItemSizeHolderSize;
for (auto kvHolder : vec) {
kvHolder->offset = offset; // :1141 回填新 offset
offset += kvHolder->computedKVSize + kvHolder->valueSize;
}
}
}
output->writeUInt32(AESCrypt::randomItemSizeHolder(ItemSizeHolderSize)); // :1147 写随机占位头
auto writtenSize = static_cast<size_t>(writePtr – originOutputPtr);
if (encrypter) { encrypter->encrypt(originOutputPtr, originOutputPtr, writtenSize); } // :1151
assert(writtenSize == totalSize); // :1154
output->seek(writtenSize – ItemSizeHolderSize);
}
这段是全库最漂亮的一段代码,务必理解:
- 重整 不调用 protobuf 重新编码。因为文件里每条记录本来就是完整、自洽的字节序列,只要按 offset 排序、把还活着的记录 memmove 到头部,无效数据(被覆盖的旧值、墓碑)自然被跳过。
- 相邻区间合并(:1121-1129):把内存中连续的多个 KV 合成一个 section,一次 memmove 搞定。N 条记录最坏 N 次 move,通常几次就够了 —— 这是从 O(N) 次系统调用式开销降到 O(段数) 次。
- 最后 assert(writtenSize == totalSize) 自检:9.1 中 prepareEncode 的纯算术预估必须与真实搬运用量完全一致。这是把「不序列化」优化闭环验证的地方。
- randomItemSizeHolder()(见 11.3)写一个随机填充头,混淆真实数据长度。
加密版本的 memmoveDictionary(Core/MMKV_IO.cpp:1159)额外处理 CFB 流状态:每个 section 用 decrypter->cloneWithStatus(*get<2>(section)) 从该 section 自己的 AESCryptStatus 恢复解密,再顺序重加密并更新状态;若 holder 类型不是 Offset(值内联在字典里),则先 MiniPBCoder::encodeDataWithObject() 把它真正落回文件 —— 这就是重整后 offset 回填被限制在 if (!encrypter) 里的原因。
9.4 fullWriteback 与 doFullWriteBack
// Core/MMKV_IO.cpp:1059
bool MMKV::fullWriteback(AESCrypt *newCrypter, bool onlyWhileExpire) {
if (m_hasFullWriteback) { return true; } // :1060 ★短路:已经干净就不必重整
if (m_needLoadFromFile) { return true; } // :1063
if (!isFileValid()) { return false; }
if (mmkv_unlikely(m_enableKeyExpire)) {
auto expiredCount = filterExpiredKeys();
if (onlyWhileExpire && expiredCount == 0) { return true; } // :1073
}
auto isEmpty = m_crypter ? m_dicCrypt->empty() : m_dic->empty();
if (isEmpty) { clearAll(); return true; } // :1080 空表直接清空
SCOPED_LOCK(m_exclusiveProcessLock);
auto preparedData = m_crypter ? prepareEncode(*m_dicCrypt) : prepareEncode(*m_dic);
auto sizeOfDic = preparedData.second;
if (sizeOfDic > 0) {
auto fileSize = m_file->getFileSize();
if (sizeOfDic + Fixed32Size <= fileSize) {
return doFullWriteBack(std::move(preparedData), newCrypter); // :1090
} else {
assert(0); assert(newCrypter == nullptr);
auto newSize = sizeOfDic + Fixed32Size – fileSize; // :1095
return expandAndWriteBack(newSize, std::move(preparedData)); // 扩容路径自带写回
}
}
return false;
}
doFullWriteBack(Core/MMKV_IO.cpp:1264 / :1312)负责收尾:重建 CodedOutputData → memmoveDictionary(或 fullWriteBackWholeData)→ m_actualSize = totalSize → recalculateCRCDigestWithIV(newIV)(内部 IncreaseSequence,触发 sequence++)→ m_hasFullWriteback = true → sync(MMKV_SYNC)。
「什么时候重整」的完整答案:
- 被动:ensureMemorySize 发现 newSize >= spaceLeft() —— 追加前发现装不下;
- 主动:业务显式调用 MMKV.trim() / reKey() / clearMemoryCache(keepSpace=false) / checkDataValid 判定需要抢救(needFullWriteback);
- 不会 有后台线程定时重整 —— MMKV 没有任何定时器,重整只发生在写路径上,因此 不会引入随机延迟尖刺以外的线程竞争,可预测性强。
9.5 trim():唯一的缩容入口
// Core/MMKV_IO.cpp:1413
void MMKV::trim() {
SCOPED_LOCK(m_lock);
SCOPED_LOCK(m_exclusiveProcessLock);
checkLoadData();
if (!isFileValid()) { ... return; }
if (m_actualSize == 0) { clearAll(); return; } // :1425
else if (m_file->getFileSize() <= m_expectedCapacity) { return; } // :1427 已经足够小
fullWriteback(); // :1431 先重整
auto oldSize = m_file->getFileSize();
auto fileSize = oldSize;
while (fileSize > (m_actualSize + Fixed32Size) * 2) { fileSize /= 2; } // :1434 ★二分找最小可用
fileSize = std::max<size_t>(fileSize, m_expectedCapacity);
if (oldSize == fileSize) { ... return; }
if (!m_file->truncate(fileSize)) { return; } // :1445
fileSize = m_file->getFileSize();
auto ptr = (uint8_t *) m_file->getMemory();
delete m_output;
m_output = new CodedOutputData(ptr + pbFixed32Size(), fileSize – Fixed32Size); // :1451
m_output->seek(m_actualSize);
}
【纠偏】「MMKV 会自动收缩文件、空间自愈」这句话要打个折
- 扩容是自动的(ensureMemorySize → expandAndWriteBack);
- 重整是自动的(同上,doFullWriteBack 顺带完成);
- 缩容只能靠业务显式调用 trim(),源码里没有任何地方自动调 trim()。
正确说法:「MMKV 通过自动重整把逻辑垃圾回收掉,文件大小呈锯齿而非单调增长;但要真正把文件缩回去,需要业务在合适时机(如切后台)调 trim()。」
9.6 场景推演:10 页的数据删到只需 9 页,那 1 页去哪了
设页大小 DEFAULT_MMAP_SIZE = 4 KB(运行期由 Core/MMKV.cpp:165 的 getPageSize() 赋值,Android 常见 4 KB、Android 15+ / arm64 可能是 16 KB)。初始 fileSize = 10 × 4 KB = 40960,删除若干 key 后有效数据只需 9 页。
结论先行:那 1 页既不会被释放,也不会被归还,它会变成 free space 被后续 append 原地复用。重整不会缩,重启也不会缩 —— 而且这个例子里连 trim() 都缩不动。
① 重整(fullWriteback)做了什么、没做什么
// Core/MMKV_IO.cpp:1323-1333 doFullWriteBack
delete m_output;
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() – Fixed32Size); // :1324 ★容量仍用 fileSize
...
m_actualSize = totalSize; // :1333 ★有效数据长度缩到 9 页
recalculateCRCDigestWithIV(...);
m_hasFullWriteback = true;
- 重整的本质是 把写指针拨回起点 + 压实有效字节:m_output 的 m_position 归零,m_actualSize 从「接近 10 页」掉到 9 页;
- 但 CodedOutputData 的容量 m_size 取的是 m_file->getFileSize(),仍是 10 页。于是 spaceLeft() = m_size – m_position(Core/CodedOutputData.cpp:97-102)凭空多出约 1 页;
- 整条重整链路里没有任何一处调用 truncate() 或 munmap()。 全仓 MMKV 目录里 grep madvise|MADV_ 命中数为 0 —— MMKV 甚至不会用 madvise(MADV_DONTNEED) 把空页还给内核;
- 所以那 1 页的命运是:留在文件里,成为写指针前方待填充的空闲区,下次 append 直接 memcpy 进去用掉,一分钱不花。只有当 spaceLeft() 不够时,ensureMemorySize(Core/MMKV_IO.cpp:402-417)才会去重整或翻倍扩容。
一句话:重整回收的是「逻辑垃圾」,不是「物理空间」。 这也是 totalSize() 与 actualSize() 两个 API 必须分开的原因 —— Java 注释说得很直白(Android/…/MMKV.java:1086-1088):“totalSize() won’t reduce after deleting key-values, call this method (trim()) after lots of deleting if you care about disk usage.”
② 退出 App 再重启,还是 10 页
重启后走构造 → MemoryFile::reloadFromFile():
// Core/MemoryFile.cpp:226-237
mmkv::getFileSize(m_diskFile.m_fd, m_size); // :226 读到真实的 40960
size_t expectedSize = std::max<size_t>(DEFAULT_MMAP_SIZE,
roundUp<size_t>(expectedCapacity, DEFAULT_MMAP_SIZE));
if (m_size < expectedSize || (m_size % DEFAULT_MMAP_SIZE != 0)) { // :229
... truncate(roundSize); // 只有这里才会改大小
} else {
auto ret = mmap(); // :237 ★原样映射 10 页
}
40960 < 4096?否。40960 % 4096 != 0?否。两个条件都不成立 → 不进 truncate 分支,直接 mmap() 整个 10 页。 随后 loadFromFile() 只按 .crc 里的 m_actualSize(9 页)解码,第 10 页继续当空闲区。
顺带一个真实边界:如果同一份文件被搬到 16 KB 页 的设备上,40960 % 16384 != 0 成立 → 进 truncate 分支,roundSize = (40960/16384 + 1) × 16384 = 49152 —— 文件被撑大到 48 KB。这条路径只会长不会短,是 Android 15+ 16 KB page size 适配下会遇到的现象。
③ 更反直觉的一点:这个场景里 trim() 也不会缩
// Core/MMKV_IO.cpp:1431-1441
fullWriteback(); // :1431
auto oldSize = m_file->getFileSize(); // 40960
auto fileSize = oldSize;
while (fileSize > (m_actualSize + Fixed32Size) * 2) { fileSize /= 2; } // :1434
fileSize = std::max<size_t>(fileSize, m_expectedCapacity);
if (oldSize == fileSize) { MMKVInfo("there's no need to trim …"); return; } // :1438
缩容判据是 「文件大小要超过有效数据的 2 倍」:40960 > (36864 + 4) × 2 = 73736?不成立 → while 一次都不进 → fileSize == oldSize → 在 :1438 直接 return,一页都省不下来。
代入几个具体值(4 KB 页):
| 9 页 = 36864 | 需 < 20476 → 否 | 不缩,40960 原样 |
| 5 页 = 20480 | 需 < 20476 → 差 4 字节,否 | 不缩 |
| 4 页 = 16384 | 成立 | 40960 → 20480(5 页),再判 20480 > 32776? 否,停 |
| 1 页 = 4096 | 连半三次:40960→20480→10240→5120,5120 > 8200? 否 → 停 | 缩到 5120(仍留 1 页余量) |
| ≈0.5 页 = 2048 | 40960→20480→10240→5120→2560,max(2560, 4096) | 缩到 4096(被 m_expectedCapacity 兜住) |
trim() 的收敛点是:最终 fileSize ∈ (a+4, (a+4)×2],再与 m_expectedCapacity 取大。也就是说 MMKV 的缩容策略天生保守 —— 宁可留一倍的余量避免频繁扩容,也不会贴着数据长度切。想一步砍到最小,只有 clearAll():
// Core/MMKV_IO.cpp:1468-1491 clearAll(keepSpace = false)
if (!keepSpace) { m_file->truncate(m_expectedCapacity); } // :1473-1474 直接回到初始页大小
writeActualSize(0, 0, newIV, IncreaseSequence); // :1483 sequence++,其他进程据此全量重载
clearMemoryCache(keepSpace); // :1490 → m_file->clearMemoryCache() → munmap + close
loadFromFile(); // :1491 重新映射
clearAllWithKeepingSpace()(Java 侧 Android/…/MMKV.java:1083)走 keepSpace = true:不 truncate、不 munmap(Core/MMKV.cpp:326-328 的 if (!keepSpace) 分支跳过 m_file->clearMemoryCache()),保留 10 页原地复用,注释里明说这是 “faster implementation”(MMKV.java:1080-1082)。
④ 那 1 页到底在消耗什么资源
| 磁盘 | 占,而且是真实块占用 | expandAndWriteBack → MemoryFile::truncate 会调 zeroFillFile()(Core/MemoryFile.cpp:155、:349-374),用 write(fd, zeros, 4096) 真写了 0,不是文件空洞(sparse hole),也没用 FALLOC_FL_PUNCH_HOLE。所以 ls -l / du 都是 10 页 |
| 虚拟地址空间 | 占 1 页 VMA,量级可忽略 | mmap() 一次性映射整个 fileSize(Core/MemoryFile.cpp:197) |
| 物理内存 / RSS | 影响极小 | MAP_SHARED 映射的是 page cache(非匿名页)。空闲页若未被弄脏,内存紧张时内核可直接回收,无需回写;MMKV 也不主动 madvise 提前释放 —— 全仓 0 命中 |
| 写放大 | 不占 | 空闲区在写指针前方,append 直接推进,重整后头一个写它的进程不需要任何预处理 |
所以「MMKV mmap 会不会吃内存」的正解是:常驻的匿名内存只有 m_dic(key → 12B holder 的 unordered_map)加解密模式下的明文值;映射区本身是文件页缓存,算 PSS 但不算不可回收内存,而且 mmap 长度是 10 页 ≠ 用掉 10 页 RAM。
⑤ 工程建议(照源码来,别照感觉来)
- 大量删除后不要指望自动回收空间:grep -n "trim()" Core 只能找到定义,运行期无人调用;
- 想回收 → 在切后台 / 冷启动空闲时调 trim(),并且心里清楚它需要 actualSize < fileSize / 2 才生效,缩完还留一倍余量;
- 数据可整体作废 → 用 clearAll()(会 truncate 回初始页 + munmap 重映射,有 IO 抖动)而不是逐条 removeValueForKey;
- 预知容量 → 用 mmkvWithID(…, expectedCapacity)(Core/MMKV.cpp:87 的 m_expectedCapacity)一次申请到位,避开反复翻倍;它是 trim() 的下限(:1437),不会被缩没;
- 监控 → 看 totalSize() 而不是 actualSize(),只有前者反映真实磁盘占用。
一句话面试版:删数据在 MMKV 里只是「追加了一条 valueSize == 0 的墓碑」,空间回收靠重整,而重整只把写指针拨回起点、不改文件大小;空出来的页变成 free space 被后续写复用,重启后照样映射那么多页。要真缩文件只能手动 trim(),且它的判据是「有效数据不足文件一半」,天生保守 —— 这是 totalSize() 和 actualSize() 分成两个 API 的根本原因。 (墓碑的代码正身:Core/MMKV_IO.cpp:749-760,static MMBuffer nan; + appendDataWithKey(nan, key, …),同时置 m_hasFullWriteback = false。)
10. 数据安全:CRC 校验与自动自愈
10.1 四个 CRC 助手函数
// Core/MMKV.cpp:405 全量校验(加载时用)
bool MMKV::checkFileCRCValid(size_t actualSize, uint32_t crcDigest) {
auto ptr = (uint8_t *) m_file->getMemory();
if (ptr) {
m_crcDigest = (uint32_t) CRC32(0, (const uint8_t *) ptr + Fixed32Size, (uint32_t) actualSize);
if (m_crcDigest == crcDigest) { return true; }
MMKVError("check crc [%s] fail, crc32:%u, m_crcDigest:%u", ...);
}
return false;
}
// Core/MMKV.cpp:418 重整后:全量重算 + sequence++
void MMKV::recalculateCRCDigestWithIV(const void *iv) {
m_crcDigest = 0;
m_crcDigest = (uint32_t) CRC32(0, ptr + Fixed32Size, (uint32_t) m_actualSize);
writeActualSize(m_actualSize, m_crcDigest, iv, IncreaseSequence); // :423
}
// Core/MMKV.cpp:427 原地覆盖后:全量重算,但 sequence 不变
void MMKV::recalculateCRCDigestOnly() {
...
writeActualSize(m_actualSize, m_crcDigest, nullptr, KeepSequence); // :432
}
// Core/MMKV.cpp:436 ★热路径:增量续算
void MMKV::updateCRCDigest(const uint8_t *ptr, size_t length) {
if (ptr == nullptr) { return; }
m_crcDigest = (uint32_t) CRC32(m_crcDigest, ptr, (uint32_t) length); // :440
writeActualSize(m_actualSize, m_crcDigest, nullptr, KeepSequence); // :442
}
注意 updateCRCDigest 里 CRC32(m_crcDigest, ptr, length) 的第一个参数:CRC32 是滚动哈希,把上一次的输出作为种子继续算,结果等价于从头算整个前缀。所以 热路径的 CRC 成本只与本次新增字节数成正比,而不是 O(文件大小)。这是「有校验但不掉性能」的根因。
另一个细节:updateCRCDigest 用的是 KeepSequence,即 普通 append 不推进 sequence;只有重整、clearAll、reKey 这类「offset 全变」的操作才 IncreaseSequence。语义与 8.1 的三种状态严格对应。
10.2 writeActualSize:账本的落笔处
// Core/MMKV_IO.cpp:487
bool MMKV::writeActualSize(size_t size, uint32_t crcDigest, const void *iv, bool increaseSequence) {
oldStyleWriteActualSize(size); // :489 ★先写数据文件头 4 字节(降级兼容)
if (!m_metaFile->isFileValid()) { return false; }
bool needsFullWrite = false;
m_actualSize = size;
m_metaInfo->m_actualSize = static_cast<uint32_t>(size);
m_crcDigest = crcDigest;
m_metaInfo->m_crcDigest = crcDigest;
if (m_metaInfo->m_version < MMKVVersionSequence) { m_metaInfo->m_version = MMKVVersionSequence; needsFullWrite = true; }
if (mmkv_unlikely(iv)) { // :505 有新的随机 IV
memcpy(m_metaInfo->m_vector, iv, sizeof(m_metaInfo->m_vector));
if (m_metaInfo->m_version < MMKVVersionRandomIV) { m_metaInfo->m_version = MMKVVersionRandomIV; }
needsFullWrite = true;
}
if (mmkv_unlikely(increaseSequence)) { // :513
m_metaInfo->m_sequence++;
m_metaInfo->m_lastConfirmedMetaInfo.lastActualSize = static_cast<uint32_t>(size); // :515
m_metaInfo->m_lastConfirmedMetaInfo.lastCRCDigest = crcDigest; // :516
...
needsFullWrite = true;
}
...
if (mmkv_unlikely(needsFullWrite)) {
m_metaInfo->write(m_metaFile->getMemory()); // :536 整页写
} else {
m_metaInfo->writeCRCAndActualSizeOnly(m_metaFile->getMemory()); // :538 ★只改 8 字节
}
return true;
}
热路径的账本写入只有 8 字节(m_crcDigest + m_actualSize),并且 m_lastConfirmedMetaInfo 只在 increaseSequence 时才更新 —— 含义是:「上一个已被确认的完整状态」是上一次重整/清空后的快照,而不是每一次 append。因此崩溃时最多回退到上一次重整,中间已 append 的数据仍然在文件里、并且会被第 10.3 节的 CRC 精确定位到损坏点。
10.3 checkDataValid:三级自愈
// Core/MMKV_IO.cpp:230
void MMKV::checkDataValid(bool &loadFromFile, bool &needFullWriteback) {
auto fileSize = m_file->getFileSize();
auto checkLastConfirmedInfo = [&] {
if (m_metaInfo->m_version >= MMKVVersionActualSize) {
// ① downgrade & upgrade support
uint32_t oldStyleActualSize = 0;
memcpy(&oldStyleActualSize, m_file->getMemory(), Fixed32Size); // :237
if (oldStyleActualSize != m_actualSize) {
if (oldStyleActualSize < fileSize && (oldStyleActualSize + Fixed32Size) <= fileSize) {
if (checkFileCRCValid(oldStyleActualSize, m_metaInfo->m_crcDigest)) {
MMKVInfo("looks like [%s] been downgrade & upgrade again", ...);
loadFromFile = true;
writeActualSize(oldStyleActualSize, m_metaInfo->m_crcDigest, nullptr, KeepSequence);
return; // :246
}
}
}
// ② 回退到 lastConfirmed
auto lastActualSize = m_metaInfo->m_lastConfirmedMetaInfo.lastActualSize; // :253
if (lastActualSize < fileSize && (lastActualSize + Fixed32Size) <= fileSize) {
auto lastCRCDigest = m_metaInfo->m_lastConfirmedMetaInfo.lastCRCDigest;
if (checkFileCRCValid(lastActualSize, lastCRCDigest)) {
loadFromFile = true;
writeActualSize(lastActualSize, lastCRCDigest, nullptr, KeepSequence); // :258
}
}
}
};
m_actualSize = readActualSize(); // :270
if (m_actualSize < fileSize && (m_actualSize + Fixed32Size) <= fileSize) {
if (checkFileCRCValid(m_actualSize, m_metaInfo->m_crcDigest)) {
loadFromFile = true; // :274 一切正常
} else {
checkLastConfirmedInfo(); // :276
if (!loadFromFile) {
auto strategic = onMMKVCRCCheckFail(m_mmapID); // :279 问业务
if (strategic == OnErrorRecover) {
loadFromFile = true;
needFullWriteback = true; // :282 ★贪心恢复
}
}
}
} else {
MMKVError("check [%s] error: %zu size in total, file size is %zu", ...);
checkLastConfirmedInfo(); // :290
if (!loadFromFile) {
auto strategic = onMMKVFileLengthError(m_mmapID); // :293
if (strategic == OnErrorRecover) {
m_actualSize = fileSize – Fixed32Size; // :296 别越界读
loadFromFile = true;
needFullWriteback = true;
}
}
}
}
自愈的完整优先级(背下来即可应付追问):
- OnErrorRecover → loadFromFile=true, needFullWriteback=true,走 greedyDecodeMap 尽可能抢救损坏点之前的所有记录,然后立即 fullWriteback 把有效数据重写落盘;
- OnErrorDiscard(默认 :277 之后仍为 false 时) → 走 loadFromFile 的 else 分支,writeActualSize(0, 0, nullptr, IncreaseSequence) 整个文件作废(Core/MMKV_IO.cpp:133)。
错误处理入口类型定义在 Core/MMKVPredef.h:146:
enum MMKVRecoverStrategic : int { OnErrorDiscard = 0, OnErrorRecover, };
typedef MMKVRecoverStrategic (*ErrorHandler)(const std::string &mmapID, MMKVErrorType errorType); // :166
10.4 meta 文件自身的「半写」检测
// Core/MMKV_IO.cpp:205
// the meta file is in specious status
if (m_metaInfo->m_version >= MMKVVersionHolder) {
MMKVWarning("meta file [%s] in specious state, version %u, flags 0x%llx", ...);
m_metaInfo->m_version = MMKVVersionActualSize; // :210 回退到「上一个已知版本」
m_metaInfo->m_flags = 0; // :211
m_metaInfo->write(m_metaFile->getMemory());
}
版本号枚举(Core/MMKVMetaInfo.hpp:47-50):MMKVVersionNext = 5,MMKVVersionHolder = MMKVVersionNext + 1。
这是一招很巧的「哨兵版本」:正常写入的 m_version 永远 < MMKVVersionHolder。如果读出来的版本号 大于等于 这个不可能值,说明这块页是文件系统为新文件填的 全 0/未初始化内容之外的垃圾(或全 1 之类的异常态),即 meta 页本身处于「写了一半」的 specious 状态。此时不猜、不崩,直接把版本回退为 MMKVVersionActualSize(“the last version we don’t check meta file”,:209 注释),清掉 flags,让后续 checkDataValid 用数据文件 + CRC 来判断真相。
11. AES-CFB128 加密
11.1 封装与密钥
// Core/aes/AESCrypt.h:57
// a AES CFB-128 encrypt-decrypt full-duplex wrapper
class AESCrypt {
...
void getCurStatus(AESCryptStatus &status); // :82
void statusBeforeDecrypt(const void *input, const void *output, size_t length,
AESCryptStatus &status); // :83
AESCrypt cloneWithStatus(const AESCryptStatus &status) const; // :85
void resetStatus(const AESCryptStatus &status); // :88
static void fillRandomIV(void *vector); // :93
static uint32_t randomItemSizeHolder(uint32_t size); // :94
};
// Core/aes/AESCrypt.h:48
struct AESCryptStatus { uint8_t m_number; uint8_t m_vector[AES_KEY_LEN]; }; // #pragma pack(1)
- 算法:AES-128-CFB(128bit 全反馈),密钥 AES_KEY_LEN = 16(Core/MMKVPredef.h:235),实现来自内嵌的 OpenSSL aes 模块(Core/aes/openssl/),aarch64 上被替换为 ARMv8 硬件指令(见 2.3)。
- AESCryptStatus 是点睛之笔:CFB 模式在 128bit 全反馈下,第 n 块的 keystream 依赖第 n-1 块密文,天生无法随机访问。把 (m_number, m_vector) 这两个「流位置状态」随每条大记录一起存进 KeyValueHolderCrypt,就等于给每条记录存了一个 “随机访问存档点”,从此可以用 cloneWithStatus() 从任意 offset 独立解密(Core/MMKV_IO.cpp:1159 的加密版 memmoveDictionary 依赖此能力)。
- thread_local AESCryptStatus t_status;(Core/MMKV_IO.cpp:577):写线程在 append 前用 m_crypter->getCurStatus(t_status)(:850)抓拍当前流状态,供 holder 记录存档点。
11.2 密钥存在哪里
AESCrypt 构造时 memcpy(m_key, key, …)(Core/aes/AESCrypt.cpp:51),m_key 是 native 堆上的 16 字节数组。Java 层 cryptKey 字符串在 JNI 调用中转为 std::string,native-bridge.cpp 里用 String2MMKVPath_t + SCOPE_EXIT 之类的作用域保护,函数返回即析构并(尽力)抹掉临时串。所以:
- 密钥 不进 Java 堆、不进 dex、不进 SharedPreferences,反射和 Java 层 hook 拿不到;
- 但 native 内存 dump(Frida hook AESCrypt 构造)依然能拿到。MMKV 的加密定位是「防君子不防小人」的 静态数据保护(防止文件被直接读取/拷贝解析),不是 密钥管理体系。生产级敏感数据请再套一层信封加密。
11.3 IV 随机化与长度伪装
// Core/aes/AESCrypt.cpp:109
void AESCrypt::fillRandomIV(void *vector) {
srand((unsigned) time(nullptr));
int *ptr = (int *) vector;
for (uint32_t i = 0; i < AES_KEY_LEN / sizeof(int); i++) { ptr[i] = rand(); } // :116
}
IV 存进 meta 的 m_vector[16](Core/MMKVMetaInfo.hpp:57),加载时 resetIV()(Core/MMKV_IO.cpp:69)。每次 fullWriteback / clearAll(Core/MMKV_IO.cpp:1479)都会换一个新的随机 IV,因此同一份数据两次重整后的密文不同,削弱「已知明文/固定前缀」类分析。
// Core/aes/AESCrypt.cpp:33 随机化的 ItemSizeHolder
uint32_t AESCrypt::randomItemSizeHolder(uint32_t size) {
constexpr uint32_t ItemSizeHolders[] = {0, 0x80, 0x4000, 0x200000, 0x10000000, 0}; // :34
auto ItemSizeHolderMin = ItemSizeHolders[size – 1];
auto ItemSizeHolderMax = ItemSizeHolders[size] – 1;
srand((unsigned) time(nullptr));
auto result = static_cast<uint32_t>(rand());
result = result % (ItemSizeHolderMax – ItemSizeHolderMin + 1);
result += ItemSizeHolderMin;
return result;
}
文件头第 5–8 字节本来是「字典序列化结果的长度」,会被攻击者用来推断条目数。MMKV 把它替换成 同 varint 字节数下的随机值:{0, 0x80}、{0x80, 0x4000}… 是按 varint 编码位数分段的区间,保证随机值 占用的字节数与真实值一致(size 参数就是所需字节数),因此不破坏后续解析。读取时 MiniPBCoder::decodeOneMap 只是 m_inputData->readInt32() 把它 吃掉丢弃(Core/MiniPBCoder.cpp:511),从不参与逻辑 —— 所以伪装无害。这是一个「用兼容性换隐私」的小设计,很值得在面试中讲。
顺带一个真实的工程取舍:srand(time) + rand() 并非密码学安全随机源。它的目的只是「每次不同」,IV 的作用在 CFB 下也主要是打散密文而非语义安全。若要做严谨评估,这里应当换成 getrandom/arc4random。
11.4 reKey:换密钥 = 一次全量重整
// Core/MMKV_IO.cpp:1345(骨架)
bool MMKV::reKey(const string &cryptKey) {
SCOPED_LOCK(m_lock);
SCOPED_LOCK(m_exclusiveProcessLock);
checkLoadData();
if (!isFileValid()) { MMKVWarning("[%s] file not valid", ...); return false; } // :1350
if (m_crypter) {
if (cryptKey.length() > 0) {
if (cryptKey == oldKey) { return true; } // :1358 同键直接成功
auto newCrypt = new AESCrypt(cryptKey.data(), cryptKey.length());
m_hasFullWriteback = false; // :1364 ★强制重整
ret = fullWriteback(newCrypt); // :1365 重整即"用新钥重写全部"
if (ret) { delete m_crypter; m_crypter = newCrypt; }
else { delete newCrypt; } // :1370 失败则保留旧钥
} else {
ret = fullWriteback(InvalidCryptPtr); // :1377 解密为明文
}
} else {
if (cryptKey.length() > 0) {
auto newCrypt = new AESCrypt(cryptKey.data(), cryptKey.length());
ret = fullWriteback(newCrypt); // :1392 明文转加密
} else { return true; }
}
if (ret) { clearMemoryCache(); } // :1407 ★reKey 后字典失效
return ret;
}
MMKV 没有为「换密钥」写任何专门的遍历逻辑,而是复用重整:m_hasFullWriteback = false 逼 fullWriteback 干活,doFullWriteBack 内部对整段数据「旧钥解密 → 新钥加密 → 换 IV → 重算 CRC → sequence++ → msync」。因此 加密/解密/换钥三件事共用一条代码路径,出错面极小。最后 clearMemoryCache() 是因为 holder 里存的 cryptStatus 已经全部作废。
12. 进阶能力:过期、compareBeforeSet、内容通知、ashmem
12.1 Key 过期(TTL)
- 开关:MMKV.enableAutoKeyExpire(expireDurationInSeconds) → 落到 m_enableKeyExpire 与 meta 的 m_flags(Core/MMKVMetaInfo.hpp:70 EnableKeyExipre = 1 << 0),下次 loadMetaInfoAndCheck() 读回(Core/MMKV_IO.cpp:216)。
- 存储:写值时 在 value 尾部追加 4 字节 little-endian 过期时间戳(Core/MMKV.cpp:461),读时用 getDataWithoutMTimeForKey() 剥掉这 4 字节(Core/MMKV_IO.cpp:567)。
- 清理:filterExpiredKeys()(Core/MMKV_IO.cpp:1820)在 ensureMemorySize 与 fullWriteback 前被动触发。
- 限制:enableCompareBeforeSet 与过期 互斥,源码显式关闭前者:
// Core/MMKV_IO.cpp:217
if (m_enableKeyExpire && m_enableCompareBeforeSet) {
MMKVError("enableCompareBeforeSet will be invalid when Expiration is on");
m_enableCompareBeforeSet = false;
}
12.2 compareBeforeSet
见 6.2 第 ① 分支:开启后(MMKV.enableCompareBeforeSet(),Core/MMKV_IO.cpp:1903),对已存在 key 先做 oldValueData == data 的字节比较,相同则 直接 return true。适合「状态值频繁 set 但极少真变」的场景,能显著减少文件膨胀和 CRC 计算。代价:每次 set 多一次 MMBuffer 构造 + memcmp,热路径变长,因此默认关闭。
12.3 跨进程内容变更通知
// MMKV.java:1616(Javadoc 本身就是最权威的答案)
/**
* Register for MMKV inter-process content change notification.
* The notification will trigger only when any method is manually called on the MMKV instance.
* For example {@link #checkContentChangedByOuterProcess()}.
*/
public static void registerContentChangeNotify(MMKVContentChangeNotification notify) { // :1623
gContentChangeNotify = notify;
setWantsContentChangeNotify(gContentChangeNotify != null); // :1625
}
// MMKV.java:1636 —— native 回调进 Java 的落点
private static void onContentChangedByOuterProcess(String mmapID) {
if (gContentChangeNotify != null) {
gContentChangeNotify.onContentChangedByOuterProcess(mmapID); // :1638
}
}
官方 Javadoc 那句话(MMKV.java:1618)值得逐字读:“trigger only when any method is manually called on the MMKV instance”。
触发点在 Core/MMKV_IO.cpp:330 / :344 的 notifyContentChanged()(实现见 Core/MMKV.cpp:283):只有 checkLoadData() 检测到别的进程改了数据,才会回调。
12.3b checkProcessMode 之外的另一个手动入口
// MMKV.java:1645(注释)+ :1647
/** Check inter-process content change manually. */
public native void checkContentChangedByOuterProcess();
这就是官方给的「主动拉一次一致性检查」入口,native 实现内部即调用 checkLoadData()(Core/MMKV_IO.cpp:305)。想要「伪 push」,就定时调它 —— 成本仅是一次 40 字节的 meta memcpy 比较。
MMKV.java:1613 的注释也印证了触发时机:// trigger by getXXX() or setXXX() or checkContentChangedByOuterProcess()。
必须理解的关键限制:MMKV 没有 push 机制。notifyContentChanged 只会在 本进程自己发生读或写(从而进入 checkLoadData)时被触发。也就是说:
「其他进程改了数据,本进程不会自动收到通知」—— 想要「实时感知」,要么轮询读一次(成本极低),要么自己在写方额外广播(ContentObserver/Broadcast)。这是很多团队接入 MMKV 做多进程配置同步时的第一个坑。
12.4 ashmem 模式与 MMKVContentProvider
【纠偏】MMKVContentProvider 与「多进程性能方案」无关
面经里常说「MMKV 不用 ContentProvider 是因为它慢」,然后有人看到库里真有 MMKVContentProvider.java 就困惑了。真相是:
- MMKVContentProvider 只服务于 mmkvWithAshmemID() 这一种 跨应用共享内存 场景:它的作用是在进程/应用之间 传递一个 ashmem 的 fd(配合 ParcelableMMKV 把 fd 打包进 Bundle),完全不参与数据读写路径。
- 常规多进程(同一 App 多进程共享 MMKV)走的是 mmap 同一磁盘文件 + flock + meta 三元组,跟 ContentProvider 一点关系都没有。
所以正确说法是:「MMKV 的多进程同步不依赖 Binder/ContentProvider」,而 不是 「MMKV 里没有 ContentProvider」。
ashmem 在源码里的特殊性(Core/MemoryFile_Android.cpp、Core/MMKV_Android.cpp):
- 创建:优先 ASharedMemory_create(通过 dlopen("libandroid.so") + dlsym 动态取,Android O+),降级 ashmem_create_region,再降级 open("/dev/ashmem") + ioctl(ASHMEM_SET_NAME/SET_SIZE);
- 不支持 ftruncate 扩容(MemoryFile::truncate 直接返回 false,reloadFromFile 开头 if (m_fileType == MMFILE_TYPE_ASHMEM) return; —— Core/MemoryFile.cpp:209)→ 所以 ashmem 模式必须 一开始就给足容量;
- 不支持 flock → FileLock(fd, true /*isAshmem*/) 走 fcntl 记录锁(Core/InterProcessLock_Android.cpp:47);
- doCleanMemoryCache(false) 对 ashmem 直接跳过(Core/MemoryFile.cpp:250),避免误释放共享内存。
12.5 其它实用能力速查
| 枚举全部 key | allKeys() | Core/MMKV.cpp → 遍历字典 key |
| 统计条数 | count() | m_dic->size() |
| 是否含某 key | containsKey(key) | find 判存 |
| 分页枚举 | keys(size_t pageSize, long lastOffset, …) | 按 offset 分批,避免一次性构造大数组 |
| 备份 | backupMMKVFromID(…) | MMKV_BACKUP 模式 + mmkv::copyFile(Core/MMKV_IO.cpp:1537 附近) |
| 原样拷贝 | MMKV.copyMMKV(fromID, toID) | 拷贝数据文件 + .crc |
| 删库 | MMKV.removeStorageByID(id) | Core/MMKV_IO.cpp:1537 |
| 文件健康检查 | MMKV.isFileValid(id) | Core/MMKV_IO.cpp:1494 |
| 锁 | lock()/unlock()/tryLock() | Core/MMKV.cpp:1181/1185/1189(就是暴露第 ③ 层独占进程锁给业务做复合操作的原子性) |
MMKV.lock() 语义:它拿的是 跨进程独占文件锁,不是进程内锁。业务用它来把「读-改-写」包成原子操作,替代分布式锁。这是 MMKV 常被忽略但非常实用的能力(Core/MMKV.cpp:1181)。
13. 与 SharedPreferences 的源码级对比
| 存储格式 | XML 全量文本 | 追加式 protobuf wire-format 二进制(MiniPBCoder.cpp:506) |
| 落盘方式 | write() 系统调用 + 全量重写 | MAP_SHARED mmap + memcpy(MemoryFile.cpp:197) |
| 首次读 | loadFromDisk 全量解析,可能阻塞 | 懒加载,且只建 offset 索引(MMKV.cpp:120-124 注释掉构造期加载) |
| 读单值 | 查 ArrayMap<String, Object> | unordered_map 异构查找 + 零拷贝视图(KeyValueHolder.h:41) |
| 写单值 | 改内存 + 标记 dirty,最终全量序列化 | 只追加一条记录 + 8 字节 meta(MMKV_IO.cpp:822、MMKVMetaInfo.hpp:81) |
| 删除 | 全量重写 | 追加 valueSize==0 墓碑(MiniPBCoder.cpp:523) |
| 垃圾回收 | 无(每次都是全量) | 自动重整 memmoveDictionary(MMKV_IO.cpp:1104) |
| 空间归还 | 天然 | 需业务调 trim()(MMKV_IO.cpp:1413) |
| ANR | commit 同步;apply 受 QueuedWork.waitToFinish() 牵连 | 无系统调用、无等待队列(见 Q2) |
| 多进程 | MODE_MULTI_PROCESS 已废弃且不可靠 | flock + meta 三元组(MMKV_IO.cpp:305) |
| 掉电/损坏 | 无校验,XML 半写即全废 | CRC32 + lastConfirmed 回滚 + 贪心恢复(MMKV_IO.cpp:230) |
| 加密 | 无 | AES-128-CFB,密钥仅存 native(AESCrypt.h:57) |
| 单文件大小 | 无上限,性能线性劣化 | 页粒度指数扩容(MMKV_IO.cpp:435) |
| TTL | 无 | 值尾 4 字节时间戳 + filterExpiredKeys(MMKV.cpp:461) |
| 依赖 | 系统内置 | 需 native so(体积成本 ~1MB 全 ABI) |
性能差异的根源用一句话概括:SP 的复杂度是 O(整个 map)(每次落盘都要全量序列化),MMKV 的复杂度是 O(单条记录)。当数据量小的时候差别不明显,当 SharedPreferences 里有几千个 key 时,SP 的每次 apply 都要重写整个 XML —— 这就是「用着用着越来越卡」的数学原因。
14. 面试题源码级精讲(Q1–Q8 + 误区清单)
Q1:MMKV 为什么比 SharedPreferences 快?
答题框架:三层各有一个源码支点,最后再补一层「很多人漏掉的第四层」。
IO 层:mmap 消除 write 系统调用 Core/MemoryFile.cpp:197:::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)。写入 = 用户态直接 memcpy 到内核托管的 page cache 页,没有 write() 的系统调用切换、没有用户态到内核态的那一次数据拷贝。落盘交给内核异步 writeback。
序列化层:protobuf wire-format 而非 XML Core/MiniPBCoder.cpp:506 的编解码就是 varint32 len + bytes 的平铺。没有标签名、没有转义、没有 DOM。Core/CodedInputData.cpp:182 的 readRawVarint32 是手写的移位解码。
写入策略:增量追加而非全量重写(复杂度层面) Core/MMKV_IO.cpp:822 doAppendDataWithKey:写指针对 (m_output) 上 memcpy 一条记录,m_actualSize += size,updateCRCDigest(ptr, size)。单次写成本 O(本次记录大小),与已有数据量无关。 对照 SP:单次写成本 O(全部数据)。
很多人漏掉的第四层:索引常驻 + 零拷贝读 m_dic 是 key → KeyValueHolder{keySize, valueSize, offset}(Core/KeyValueHolder.h:32),读值就是 MMBuffer(basePtr + offset + computedKVSize, valueSize, MMBufferNoCopy) —— 一次指针算术,一次构造,不拷贝不 new。SP 的快是因为它在内存里存了解析好的对象,MMKV 的快是「数据本来就在内存里,而且不需要解析」。
锦上添花:硬件指令 Core/MMKV.cpp:184 CRC32 = mmkv::armv8_crc32;:172 起把 AES 换成 ARMv8 指令。校验和加密这两件「额外成本」被 CPU 指令消化掉了。
加分点:MMKV 快不是没有代价的。代价有三:(a) 追加式写入使文件产生垃圾,需要重整机制;(b) 索引需常驻内存,m_dic 与数据是两份结构;© 落盘由内核掌控,因此「已返回 true」≠「已持久化到闪存」,掉电场景不如显式 fsync。
Q2:MMKV 如何解决 SharedPreferences 的 ANR 问题?
先把 SP 的 ANR 机理讲准(这里最能区分背书和真懂):
- commit():QueuedWork.queue(awaitCommit=true) → 当前线程同步写 XML;
- apply():startWriteToDisk() 在 FileOutputStream.write() 之前把 MemoryCommit 入队 sDiskWriteTasks,写盘在 CachedThreadPoolExecutor 异步执行;
- 真正的坑:Activity.onStop() / Service.onStart() 里 QueuedWork.waitToFinish() —— 它 while (!sDiskWriteTasks.isEmpty()) { FutureTask f = sDiskWriteTasks.take(); f.run(); },同步等待所有待写的 XML 落盘。低端机 + 大数据 + 频繁切后台时,主线程就卡死在这里 → ANR。
- 所以「apply 是异步的所以不会 ANR」是错的:apply 把成本延后到了生命周期回调上。
MMKV 侧为什么结构上不可能出现这个问题:
// Core/MMKV.cpp:436 唯一的"落盘准备"动作
void MMKV::updateCRCDigest(const uint8_t *ptr, size_t length) {
m_crcDigest = (uint32_t) CRC32(m_crcDigest, ptr, (uint32_t) length);
writeActualSize(m_actualSize, m_crcDigest, nullptr, KeepSequence); // → memcpy 8 字节到 mmap 区
}
需要老实说明的边界:mmap 消除的是「同步 IO 等待」,但没消除页错误。首次访问未 fault-in 的映射页会触发 minor fault;如果内核已把这些干净页回收,访问时会变成 major fault(需读盘),此时依然可能卡住主线程 —— 只是概率与时长远低于 SP 的全量 XML 重写。另外 MemoryFile::truncate 内的 zeroFillFile 与 ftruncate 是 真系统调用,发生在写路径的扩容瞬间,属于 MMKV 唯一可见的 IO 尖刺(Core/MMKV_IO.cpp:443)。
Q3:MMKV 的多进程是怎么实现的?为什么不用 ContentProvider?
答(四层递进):
- crcDigest 变 + 文件大小不变 → partialLoadFromFile() 增量 CRC + 只解新增段 + 写指针跟进(Core/MMKV_IO.cpp:166-177);
- crcDigest 变 + 文件大小变 → 全量重载;
- sequence 变 → 别的进程做了 fullWriteback,所有 offset 失效,必须全量重载。
【纠偏】三个必须澄清的点
加分点:MMKV 的多进程不是「实时」的。 一致性只在本进程主动调 checkLoadData() 时刷新,notifyContentChanged(Core/MMKV.cpp:283)也只在读写路径上触发。官方 Javadoc 说得很清楚(MMKV.java:1618)。要做「配置变更即时生效」,得自己轮询 checkContentChangedByOuterProcess()(MMKV.java:1647)或另加广播。
Q4:mmap 会不会导致内存泄漏?MMKV 怎么管理实例内存?
先破题:mmap 本身不会「泄漏」,但会 长期占用虚拟地址空间 并让对应物理页留在 page cache 中,所以「MMKV 实例开得越多,占用越大」是事实。真正的问题是:这些映射什么时候被释放?
源码事实(v1.3.16):
// Core/MMKV.cpp:60
unordered_map<string, MMKV *> *g_instanceDic;
// Core/MMKV.cpp:257 创建即永久缓存
(*g_instanceDic)[mmapKey] = kv;
// Core/MMKV.cpp:262 唯一的批量释放入口
void MMKV::onExit() {
SCOPED_LOCK(g_instanceLock);
for (auto &pair : *g_instanceDic) {
pair.second->sync(MMKV_SYNC);
pair.second->clearMemoryCache();
delete pair.second;
}
delete g_instanceDic;
g_instanceDic = nullptr;
}
// Core/MemoryFile.cpp:248
void MemoryFile::doCleanMemoryCache(bool forceClean) {
if (m_ptr && m_ptr != MAP_FAILED) {
if (munmap(m_ptr, m_size) != 0) { ... }
}
m_ptr = nullptr; // 随后 File::close()、m_size = 0
}
【纠偏】「MMKV 对实例采用 LRU 缓存淘汰」—— 源码中不存在
v1.3.16 没有任何 LRU:容器是裸 unordered_map,无访问计数、无淘汰队列。释放映射的时机只有三个:
那 MMKV 为什么敢不做淘汰? 因为 MAP_SHARED 的文件映射页属于 page cache(file-backed clean/dirty page):内存紧张时内核可以直接丢弃干净页(下次访问再读回),不需要 MMKV 自己管。也就是说 mmap 的 RSS 是"可回收"的,与 Java 堆里的强引用对象完全不是一个性质。
正确的风险表述:MMKV 的常驻成本主要是 ① m_dic 这张 真·进程内内存 的哈希表(每条 12 字节 holder + string key + 桶开销),② 每个实例一对 fd。实例数失控时,①才是主要内存压力源。因此规约是:mmapID 必须有限可枚举,绝不要用用户 ID / 日期之类无界字符串当 mmapID。
Context 泄漏:
// MMKV.java:91
public static String initialize(@NonNull Context context) {
String rootDir = context.getFilesDir().getPath() + "/mmkv";
...
}
【纠偏】「MMKV 通过弱引用关联 Context」—— 不存在
MMKV.java 里没有 WeakReference<Context>。initialize 只 同步读取 getFilesDir() / getCacheDir() 两个字符串就再无关系;MMKV 实例本身 不持有 Context 字段。所以 MMKV 不泄漏 Activity 的原因是「压根不引用」,而不是「弱引用」。
唯一要留意的是 你自己的 MMKVHandler / MMKVContentChangeNotification 实现:MMKV.java:1614 的 gContentChangeNotify 是 静态强引用,如果你在里面持有 Activity,那就由你负责泄漏 —— 这是真实的踩坑点。
其他资源: MMKV Parcelable 的 mCreators(MMKV.java:907)是静态 HashMap<String, Parcelable.Creator<?>>,缓存反射得到的 CREATOR,属有界缓存(key 是类名),但严格说它没有上限;checkedHandleSet(MMKV.java:57)同理,元素数量等于实例数,onExit 才清。
Q5:MMKV 的数据安全性如何保证?
答题结构:写入原子性 → 检测 → 恢复 → 加密,四层,逐层给源码。
① 写入的「原子性」来自页对齐 + 双文件分离
// Core/MMKVMetaInfo.hpp:94
static_assert(sizeof(MMKVMetaInfo) <= (4 * 1024), "MMKVMetaInfo lager than one pagesize");
账本(长度、CRC、IV、sequence)单独放 .crc 文件,且整个结构 ≤ 1 页。数据文件的追加只增加 actualSize,只要账本没写成功,多出来的字节就自动不被承认。这是 MMKV 抗「写一半崩溃」的根本设计:把"提交点"收敛到一页以内的对齐写。
② 检测:CRC32
Core/MMKV.cpp:405 全量校验、:436 增量续算。校验范围是 ptr + Fixed32Size 起、长度 actualSize 的数据区,即 所有 KV 记录的字节流。同时 Core/MMKV_IO.cpp:272 还检查 actualSize < fileSize(长度合理性)与 Core/MMKV_IO.cpp:205 的 meta 版本哨兵。
③ 恢复:三级降级 + 贪心解码 + 业务决策
| L1 | MMKV_IO.cpp:273 | 正常 CRC 匹配 |
| L2 | MMKV_IO.cpp:237-246 | 降级/升级兼容:用文件头 oldStyleActualSize 重验 |
| L3 | MMKV_IO.cpp:253-258 | 回退到 m_lastConfirmedMetaInfo(上次重整/清空的快照) |
| L4 | MMKV_IO.cpp:279/293 | 问 MMKVHandler,OnErrorRecover → needFullWriteback = true |
| L5 | MMKV_IO.cpp:110 | greedyDecodeMap:只抢救损坏点之前能解析的记录,随后立即 fullWriteback 固化 |
| L6 | MMKV_IO.cpp:133 | OnErrorDiscard → writeActualSize(0,0,…) 整个文件作废,业务可重来 |
greedy vs 非 greedy 的实现差异在 Core/MiniPBCoder.cpp:530-548:非贪心先写 tmpDic 再 dic.swap(tmpDic)(全有或全无);贪心直接写目标字典(尽力而为)。
④ 加密:AES-128-CFB + 随机 IV + 长度伪装
见第 11 章。密钥只在 native 堆(AESCrypt.cpp:51 memcpy(m_key, …)),每次重整换 IV(MMKV_IO.cpp:1479),文件头长度字段随机化(AESCrypt.cpp:33)。
【纠偏】「写时复制:修改时创建新内存页,避免写入崩溃损坏原有数据」—— 不成立
唯一沾边「复制」的地方是 mmkv::copyFile()(Core/MemoryFile.cpp)用 mkstemp 临时文件 + tryAtomicRename 做 原子替换,但那服务于 backupTo() 等文件级操作,不是常规写路径。
必须补充的诚实边界(说出来才显专业):
- mmap 不保证掉电安全。encode() 返回 true 只保证「写进了 page cache」。真掉电时未回写的脏页会丢。要更强保证必须 MMKV.sync(MMKV_SYNC)(Core/MMKV.cpp:1169 → msync(MS_SYNC)),或在关键写之后调它。
- 因此「数据零丢失」的正确说法是:进程崩溃不丢(内核仍持有脏页并继续回写),极端掉电可能有秒级窗口内的丢失,且 CRC 机制保证 丢的是一致性可检测的尾部,而不是整库损坏。
Q6:MMKV 的文件会无限增长吗?
不会,但要分清三件事:垃圾回收 ≠ 空间归还。
文件大小曲线(同一实例,高频更新 100 个 key):
size
│ ╱╲ ╱╲
│ ╱╲ ╱ ╲ ╱╲ ╱ ╲ ← 翻倍扩容
│ ╱╲ ╱ ╲__ ╲__╱╲╱ ╲__ ╲__
│ ╱ ╲__ ╲__
└──────────────────────────────────→ time
append 填充 重整(memmove) 扩容
一句话答:逻辑空间会自动回收(重整),磁盘占用会自动摊还增长但不会自动缩回;要归还磁盘必须调 trim()。
顺带纠正一个说法:「仅在空间达到阈值时才触发重整」的"阈值"具体就是 newSize >= spaceLeft()(MMKV_IO.cpp:408),是"当前剩余可追加空间不足",而不是"文件超过某个大小"。另外 clearAll()(:1457)里 keepSpace=false 时会 truncate(m_expectedCapacity),这是另一个把空间还回去的路径。
Q7:MMKV 支持哪些数据类型?如何存自定义对象?存 Serializable 行不行?
Java 层 encode/decode 家族(MMKV.java:713 起):
| boolean/int/long/float/double | encode(key, v)(:713/735/756/777/798) | decodeBool/Int/Long/Float/Double |
| String | encode(key, String)(:819) | decodeString |
| byte[] | encode(key, byte[]) | decodeBytes |
| Set<String> | encode(key, Set<String>)(:841) | decodeStringSet(:855,可传 Class<? extends Set> 指定实现,如 TreeSet) |
| Parcelable | encode(key, Parcelable)(:917、:930) | decodeParcelable(key, Class)(:940) |
每个类型都有带 expireDurationInSecond 的重载,对应 native 的 _2 后缀(如 encodeBool_2,MMKV.java:723)。
Parcelable 的真实实现:
// MMKV.java:909
private byte[] getParcelableByte(@NonNull Parcelable value) {
Parcel parcel = Parcel.obtain();
value.writeToParcel(parcel, 0);
byte[] bytes = parcel.marshall();
parcel.recycle();
return bytes;
}
【纠偏】「MMKV 内部通过 protobuf 完成 Parcelable 序列化」—— 说法错误
Parcelable 对象 由 Android Parcel 序列化成 byte[],MMKV 只是把这坨 byte[] 当成一个普通的 bytes value 存进去。protobuf 的角色是 给 MMKV 自己的记录流做长度前缀编码(key/value 的 varint 头),它 不认识 你的业务对象结构,也不做对象映射。
换句话说:MMKV 的 “protobuf” 是 容器层的编码格式,不是 业务对象的序列化框架。
decodeParcelable 的反射缓存(MMKV.java:940-964):通过 tClass.getField("CREATOR") 拿 Parcelable.Creator,缓存在静态 mCreators(MMKV.java:907)里,避免每次 getClass().getField()。
Serializable? MMKV 没有 提供 Serializable 的 encode/decode 重载。要存 Serializable 只能自己转 byte[](ObjectOutputStream)。而且强烈建议 别这么干:Java 序列化格式与 serialVersionUID/类结构强耦合,一旦混淆或改类就解不出来。工程上更稳的做法是:业务对象 → JSON/Parcelable 的 byte[] → encode(key, bytes),让存储层与类结构解耦。
类型不匹配会怎样? getBool 拿到一个 string 的字节时,CodedInputData::readBool() 会抛异常,被 Core/MMKV.cpp:801 的 catch 捕获并返回 defaultValue。MMKV 不会因为类型错用而崩溃,但会静默返回默认值 —— 这是排查「值莫名变成默认值」时的一个常见真因。
Q8:如何从 SharedPreferences 迁移到 MMKV?
// Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKV.java:1166
public int importFromSharedPreferences(@NonNull SharedPreferences preferences) {
Map<String, ?> kvs = preferences.getAll();
if (kvs == null || kvs.size() <= 0) { return 0; }
for (Map.Entry<String, ?> entry : kvs.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key == null || value == null) { continue; }
if (value instanceof Boolean) { encodeBool(nativeHandle, key, (boolean) value); }
else if (value instanceof Integer) { encodeInt(nativeHandle, key, (int) value); }
else if (value instanceof Long) { encodeLong(nativeHandle, key, (long) value); }
else if (value instanceof Float) { encodeFloat(nativeHandle, key, (float) value); }
else if (value instanceof Double) { encodeDouble(nativeHandle, key, (double) value); }
else if (value instanceof String) { encodeString(nativeHandle, key, (String) value); }
else if (value instanceof Set) { encode(key, (Set<String>) value); }
else { simpleLog(MMKVLogLevel.LevelError, "unknown type: " + value.getClass()); }
}
return kvs.size();
}
【纠偏】"一行代码即可完成迁移"是营销话术,不是实现原理
源码说明它是 getAll() + 逐条 encodeXxx 的运行时搬运,因此:
工程上推荐的标准迁移三段式:
MMKV kv = MMKV.mmkvWithID("app_prefs_shared_process", MMKV.MULTI_PROCESS_MODE);
File spFile = new File(context.getDataDir(), "shared_prefs/old_name.xml");
if (spFile.exists() && !kv.getBoolean("__migrated__", false)) {
SharedPreferences old = context.getSharedPreferences("old_name", Context.MODE_PRIVATE);
int count = kv.importFromSharedPreferences(old);
kv.encode("__migrated__", true);
spFile.delete(); // 或改名留档,防止回滚时误判
}
另一个更彻底的选项:MMKV 实现了 SharedPreferences 与 SharedPreferences.Editor 两个接口(MMKV.java:52),所以可以写一个 SharedPreferences 门面直接替换 getSharedPreferences() 的返回值(官方提供 MMKVCompat),让存量代码 不改一行 就换掉实现。这才是真正的"低成本迁移"。
高频误区清单(一张表带过,面试时主动纠偏很加分)
| 实例用 LRU 缓存淘汰 | 裸 unordered_map,永久缓存,只有 onExit/clearMemoryCache 会 munmap | MMKV.cpp:60/257/262/302 |
| 修改时写时复制(创建新内存页) | MAP_SHARED,原地 memcpy,无 COW | MemoryFile.cpp:197 |
| 弱引用关联 Context | 不持有 Context,也没有 WeakReference | MMKV.java:91 |
| 全局写指针放在 mmap 内存里 | 权威长度/CRC 在 .crc 元文件;数据文件头 4 字节只为降级兼容 | MMKV_IO.cpp:473/487 |
| 多进程用 ContentProvider 做同步 | ContentProvider 只传 ashmem fd;同步靠 flock + meta | MMKVContentProvider.java、MMKV_IO.cpp:305 |
| 多进程也能原地覆盖 | onlyOneKey 显式排除 m_isInterProcess | MMKV_IO.cpp:691 |
| 文件会自动收缩 | 缩容只能靠业务调 trim() | MMKV_IO.cpp:1413 |
| 一行代码无损迁移 SP | 运行时逐条搬运,异常只打日志、返回值不代表成功 | MMKV.java:1166 |
| CRC 失败自动回滚到上次完整状态 | 先试 lastConfirmed 回滚;失败还要问业务 OnErrorRecover/Discard | MMKV_IO.cpp:253/279 |
| MMKV 完全不会 ANR | 结构上无等待队列,但扩容的 ftruncate/zeroFill 是真系统调用 | MMKV_IO.cpp:443、MemoryFile.cpp |
| 加密密钥很安全 | 只在 native 堆,可被 Frida hook AESCrypt 构造取;属静态数据保护,非 KMS | AESCrypt.cpp:51 |
| protobuf 序列化业务对象 | 只编码容器长度前缀;Parcelable 走 Parcel.marshall() | MMKV.java:909、MiniPBCoder.cpp:506 |
附录 A:关键源码索引表
路径均相对于 C:\\Users\\Administrator.DESKTOP-VOB36RN\\Downloads\\Android Project\\mmkv源码分析\\,行号为 v1.3.16 的 1-based 真实行号。
| 版本宏 | Core/MMKVPredef.h:37 | MMKV_VERSION = "v1.3.16" |
| 字典类型 | Core/MMKVPredef.h:228 | MMKVMap(异构查找) |
| 错误恢复策略 | Core/MMKVPredef.h:146/166 | MMKVRecoverStrategic / ErrorHandler |
| Java 类声明 | Android/…/MMKV.java:52 | implements SharedPreferences, Editor |
| Java 初始化 | Android/…/MMKV.java:91 / 192 / 216 | 入口 / 全参 / 加载 so |
| debug 开进程模式检查 | Android/…/MMKV.java:199 | enableProcessModeChecker() |
| mmkvWithID 家族 | Android/…/MMKV.java:344–500 | 各重载 |
| 进程模式校验 | Android/…/MMKV.java:601 | checkProcessMode + checkedHandleSet |
| encode 起点 | Android/…/MMKV.java:713 | boolean;819 String;841 Set;917 Parcelable |
| Parcelable → bytes | Android/…/MMKV.java:909 | Parcel.marshall() |
| SP 迁移 | Android/…/MMKV.java:1166 | importFromSharedPreferences |
| 变更通知 | Android/…/MMKV.java:1623 / 1636 / 1647 | 注册 / native 回调 / 手动检查 |
| native 声明块 | Android/…/MMKV.java:1642–1751 | private static native |
| JNI 注册 | Android/…/cpp/native-bridge.cpp:59 / 69 / 1101 / 1158 | JNI_OnLoad / 类名 / 方法表 |
| native 初始化 | Android/…/cpp/native-bridge.cpp:143 | jniInitialize_2 |
| 构造函数 | Core/MMKV.cpp:81 | 双文件 + 锁 + 懒加载注释 |
| 锁 enable 开关 | Core/MMKV.cpp:117 | 单进程时文件锁空操作 |
| 全局初始化 | Core/MMKV.cpp:160 | page size、ARMv8 AES/CRC32 |
| initializeMMKV | Core/MMKV.cpp:200 | ThreadOnce + mkPath |
| 实例字典 | Core/MMKV.cpp:60 / 233 / 257 | g_instanceDic |
| onExit | Core/MMKV.cpp:262 | sync + clearMemoryCache + delete |
| clearMemoryCache | Core/MMKV.cpp:302 | 主动 munmap |
| CRC 助手 | Core/MMKV.cpp:405 / 418 / 427 / 436 | 全量 / 重整 / 覆盖 / 增量 |
| set(bool,…) | Core/MMKV.cpp:451 | 过期时间戳拼接 |
| getBool | Core/MMKV.cpp:784 | shared 锁 + 零拷贝 |
| sync | Core/MMKV.cpp:1169 | 双文件 msync |
| lock/unlock/try_lock | Core/MMKV.cpp:1181/1185/1189 | 暴露跨进程独占锁 |
| loadFromFile | Core/MMKV_IO.cpp:64 | 启动体检主干 |
| partialLoadFromFile | Core/MMKV_IO.cpp:147 | 增量同步(3 步走) |
| loadMetaInfoAndCheck | Core/MMKV_IO.cpp:194 | meta 版本哨兵 |
| checkDataValid | Core/MMKV_IO.cpp:230 | 三级自愈 |
| checkLoadData | Core/MMKV_IO.cpp:305 | ★多进程一致性心脏 |
| ItemSizeHolderSize | Core/MMKV_IO.cpp:348 | = 4 |
| prepareEncode | Core/MMKV_IO.cpp:350 | 只算长度不序列化 |
| ensureMemorySize | Core/MMKV_IO.cpp:402 | 重整触发点 |
| expandAndWriteBack | Core/MMKV_IO.cpp:422 | 翻倍 + 未来预留 |
| readActualSize / oldStyleWriteActualSize | Core/MMKV_IO.cpp:455 / 473 | 长度读写 |
| writeActualSize | Core/MMKV_IO.cpp:487 | ★账本落笔 |
| getRawDataForKey | Core/MMKV_IO.cpp:543 | 零拷贝视图 |
| t_status | Core/MMKV_IO.cpp:577 | thread_local CFB 状态 |
| setDataForKey | Core/MMKV_IO.cpp:581 | ★三岔路口 |
| doAppendDataWithKey | Core/MMKV_IO.cpp:822 | ★追加正身 |
| doOverrideDataWithKey | Core/MMKV_IO.cpp:884 | 原地覆盖 |
| checkSizeForOverride | Core/MMKV_IO.cpp:966 | 覆盖前置检查 |
| appendDataWithKey(kvHolder) | Core/MMKV_IO.cpp:1001 | ★复用已编码 key |
| fullWriteback | Core/MMKV_IO.cpp:1059 | 重整入口 + 短路 |
| memmoveDictionary | Core/MMKV_IO.cpp:1104 | ★重整精髓(明文) |
| memmoveDictionary(crypt) | Core/MMKV_IO.cpp:1159 | CFB 存档点版 |
| doFullWriteBack | Core/MMKV_IO.cpp:1264 / 1312 | 收尾 + CRC + sync |
| reKey | Core/MMKV_IO.cpp:1345 | 换密钥=一次重整 |
| trim | Core/MMKV_IO.cpp:1413 | 唯一缩容 |
| clearAll | Core/MMKV_IO.cpp:1457 | 换 IV + truncate |
| isFileValid | Core/MMKV_IO.cpp:1494 | 健康检查 |
| removeStorage | Core/MMKV_IO.cpp:1537 | 删库 |
| filterExpiredKeys | Core/MMKV_IO.cpp:1820 | TTL 清理 |
| enableCompareBeforeSet | Core/MMKV_IO.cpp:1903 | 开关 |
| mmap() 本体 | Core/MemoryFile.cpp:195 | ★MAP_SHARED |
| msync() | Core/MemoryFile.cpp:184 | MS_SYNC/MS_ASYNC |
| truncate() 回滚 | Core/MemoryFile.cpp:160 | 失败还原长度 |
| reloadFromFile() | Core/MemoryFile.cpp:207 | 锁升级 + 重映射 |
| doCleanMemoryCache() | Core/MemoryFile.cpp:248 | munmap + close |
| FileLock::platformLock | Core/InterProcessLock.cpp:85 | ★flock + 君子协议 |
| platformUnLock | Core/InterProcessLock.cpp:128 | 退回 shared 锁 |
| ashmemLock | Core/InterProcessLock_Android.cpp:47 | fcntl 记录锁 |
| KeyValueHolder | Core/KeyValueHolder.h:32 | 12 字节 offset 索引 |
| KeyValueHolderCrypt | Core/KeyValueHolder.h:53 | 三态 union |
| isValueStoredAsOffset | Core/KeyValueHolder.h:85 | 256B 分界 |
| decodeOneMap | Core/MiniPBCoder.cpp:506 | ★墓碑 + greedy |
| MMKVMetaInfo | Core/MMKVMetaInfo.hpp:54–74 | 账本结构 |
| writeCRCAndActualSizeOnly | Core/MMKVMetaInfo.hpp:81 | 热路径 8 字节 |
| static_assert 页约束 | Core/MMKVMetaInfo.hpp:94 | ★原子性根基 |
| AESCryptStatus | Core/aes/AESCrypt.h:48 | 流位置存档点 |
| CFB-128 注释 | Core/aes/AESCrypt.h:57 | 算法出处 |
| randomItemSizeHolder | Core/aes/AESCrypt.cpp:33 | 长度伪装 |
| fillRandomIV | Core/aes/AESCrypt.cpp:109 | IV 随机化 |
| Android ashmem 构造 | Core/MMKV_Android.cpp:90 | 双 fd 双锁 |
| checkProcessMode() | Core/MMKV_Android.cpp:222 | 独占 vs 共享探测 |
附录 B:一图流总结
#mermaid-svg-1rOwywQZRiyxp9qd{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1rOwywQZRiyxp9qd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1rOwywQZRiyxp9qd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1rOwywQZRiyxp9qd .error-icon{fill:#552222;}#mermaid-svg-1rOwywQZRiyxp9qd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1rOwywQZRiyxp9qd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1rOwywQZRiyxp9qd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1rOwywQZRiyxp9qd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1rOwywQZRiyxp9qd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1rOwywQZRiyxp9qd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1rOwywQZRiyxp9qd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1rOwywQZRiyxp9qd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1rOwywQZRiyxp9qd .marker.cross{stroke:#333333;}#mermaid-svg-1rOwywQZRiyxp9qd svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1rOwywQZRiyxp9qd p{margin:0;}#mermaid-svg-1rOwywQZRiyxp9qd .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-1rOwywQZRiyxp9qd .cluster-label text{fill:#333;}#mermaid-svg-1rOwywQZRiyxp9qd .cluster-label span{color:#333;}#mermaid-svg-1rOwywQZRiyxp9qd .cluster-label span p{background-color:transparent;}#mermaid-svg-1rOwywQZRiyxp9qd .label text,#mermaid-svg-1rOwywQZRiyxp9qd span{fill:#333;color:#333;}#mermaid-svg-1rOwywQZRiyxp9qd .node rect,#mermaid-svg-1rOwywQZRiyxp9qd .node circle,#mermaid-svg-1rOwywQZRiyxp9qd .node ellipse,#mermaid-svg-1rOwywQZRiyxp9qd .node polygon,#mermaid-svg-1rOwywQZRiyxp9qd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1rOwywQZRiyxp9qd .rough-node .label text,#mermaid-svg-1rOwywQZRiyxp9qd .node .label text,#mermaid-svg-1rOwywQZRiyxp9qd .image-shape .label,#mermaid-svg-1rOwywQZRiyxp9qd .icon-shape .label{text-anchor:middle;}#mermaid-svg-1rOwywQZRiyxp9qd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1rOwywQZRiyxp9qd .rough-node .label,#mermaid-svg-1rOwywQZRiyxp9qd .node .label,#mermaid-svg-1rOwywQZRiyxp9qd .image-shape .label,#mermaid-svg-1rOwywQZRiyxp9qd .icon-shape .label{text-align:center;}#mermaid-svg-1rOwywQZRiyxp9qd .node.clickable{cursor:pointer;}#mermaid-svg-1rOwywQZRiyxp9qd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1rOwywQZRiyxp9qd .arrowheadPath{fill:#333333;}#mermaid-svg-1rOwywQZRiyxp9qd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1rOwywQZRiyxp9qd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1rOwywQZRiyxp9qd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1rOwywQZRiyxp9qd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1rOwywQZRiyxp9qd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1rOwywQZRiyxp9qd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1rOwywQZRiyxp9qd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1rOwywQZRiyxp9qd .cluster text{fill:#333;}#mermaid-svg-1rOwywQZRiyxp9qd .cluster span{color:#333;}#mermaid-svg-1rOwywQZRiyxp9qd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1rOwywQZRiyxp9qd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1rOwywQZRiyxp9qd rect.text{fill:none;stroke-width:0;}#mermaid-svg-1rOwywQZRiyxp9qd .icon-shape,#mermaid-svg-1rOwywQZRiyxp9qd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1rOwywQZRiyxp9qd .icon-shape p,#mermaid-svg-1rOwywQZRiyxp9qd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1rOwywQZRiyxp9qd .icon-shape .label rect,#mermaid-svg-1rOwywQZRiyxp9qd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1rOwywQZRiyxp9qd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1rOwywQZRiyxp9qd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1rOwywQZRiyxp9qd :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
读路径
decodeXxx(k)
ThreadLock + Shared flock
checkLoadData()
dic.find(k) → offset
MMBuffer NoCopy 视图
CodedInputData 单值解码
写路径
是
否
是
否
空间不足
足够
encode(k,v)
MMBuffer 编码堆上临时 buffer
setDataForKeyThreadLock + Exclusive flock
checkLoadData()同步他进程增量
compareBeforeSet值相同?
return true 零写入
单 key且非多进程?
override 原地覆盖
append 追加
ensureMemorySize
prepareEncode→ expandAndWriteBack→ memmoveDictionary
doFullWriteBacksequence++ + msync
memcpy 进 mmap 区
updateCRCDigest增量 CRC
writeCRCAndActualSizeOnly写 8 字节到账本
最后凝缩成 5 条,够应付任何追问:
网硕互联帮助中心







评论前必须登录!
注册