云计算百科
云计算领域专业知识百科平台

MMKV 源码级剖析(基于 v1.3.16 真实源码)

分析对象

  • 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)。凡与流传甚广的「八股说法」不符之处,均显式标注 【纠偏】。


目录

  • 总体架构与分层
  • 初始化链路:Java → JNI → C++
  • 实例管理与缓存(不是 LRU)
  • mmap 与文件布局
  • 序列化层:MiniPBCoder / CodedInOutData / KeyValueHolder
  • 写路径全链路
  • 读路径全链路
  • 多进程一致性与四层锁模型
  • 空间管理:扩容、重整与收缩
  • 数据安全:CRC 校验与自动自愈
  • AES-CFB128 加密
  • 进阶能力:过期、compareBeforeSet、内容通知、ashmem
  • 与 SharedPreferences 的源码级对比
  • 面试题源码级精讲(Q1–Q8 + 误区清单)
  • 附录 A:关键源码索引表
  • 附录 B:一图流总结

  • 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?

  • 符号不用按 Java_com_tencent_mmkv_MMKV_xxx 规则命名,C++ 侧可自由放进 namespace mmkv;
  • 符号不导出,逆向后难以定位,这是第一层自保护;
  • 查表比按名解析快(对首次调用有意义)。
  • 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 (...) { ... }
    }
    }

    三个必考点:

  • 删除 = 追加一条 valueSize == 0 的墓碑记录。MMKV 全程只有 append,没有原地删除;空位靠第 9 章的「重整」回收。这与 LSM-tree 的思路一致。
  • greedy 与 non-greedy 的区别就是「数据恢复策略」。正常加载走 decodeMap(非 greedy,先建临时字典再 swap,遇到损坏字节则整表放弃);CRC 校验失败但用户选择 OnErrorRecover 时走 greedyDecodeMap,把损坏点之前能解析出来的记录全部留下,尽可能抢救数据。见 Core/MMKV_IO.cpp:103-121。
  • position != 0 就是 多进程增量同步的解码入口(partialLoadFromFile 传 oldActualSize)。

  • 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 成本 =

  • 一次 unordered_map::find;
  • 两次 memcpy(key 与 value)到 mmap 区;
  • 一次 CRC32 增量计算(aarch64 上是硬件指令);
  • 一次 memcpy 写 4+4 字节的 meta(writeCRCAndActualSizeOnly,Core/MMKVMetaInfo.hpp:81)。
  • 没有 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 删除、清空、改名

    API源码行为
    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,锁是空操作,所以一次读的代价就是:

  • checkLoadData() 里的 if (!m_isInterProcess) return;(Core/MMKV_IO.cpp:313)——一次分支;
  • 一次哈希查找;
  • 一次指针算术构造 MMBuffer(NoCopy);
  • CodedInputData::readBool() 之类的 单值解码。
  • 与 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:CRC32(旧digest, 新增区, 新增长度) —— CRC32 的数学性质允许这样续算,所以校验成本只和 新增字节数 相关,而不是全文件。这是 MMKV 多进程能高频同步的关键。
  • 增量解码:greedyDecodeMap(dic, buffer, position) 靠 CodedInputData::seek(position) 跳过旧前缀,只解析新追加的记录(见 5.6 的 if (position) { m_inputData->seek(position); })。
  • 写指针对齐:m_output->seek(addedSize),否则本进程下次写会覆盖别人刚追加的数据。这一步忘了就是数据错乱,也是很多自研 KV 存储踩过的坑。
  • 任何一步不满足(长度不合法、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
    }

    两处细节值得在面试中主动点出来:

  • 不只是「装不下就翻倍」,还会额外预留 futureUsage = avgItemSize * max(8, N/2)。也就是说 MMKV 会预判「未来还要写一半现有条目那么多数据」,从而避免刚重整完马上又满、频繁触发全量重写。这是典型的 摊还分析(amortized) 设计。
  • truncate 失败 直接 return false,本次写入整体失败并回滚 —— 老数据完好。MemoryFile::truncate 内部还会把文件长度 ftruncate 回原尺寸(Core/MemoryFile.cpp:160)。「扩容失败不损坏数据」是被源码保证的,不是靠运气。
  • 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 &section : 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 页):

    m_actualSize首减半条件 a < fileSize/2 – 4trim() 结果
    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 页到底在消耗什么资源
    资源是否被那 1 页占用源码/OS 依据
    磁盘 占,而且是真实块占用 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;
    }
    }
    }
    }

    自愈的完整优先级(背下来即可应付追问):

  • 正常路径:actualSize 合理且 CRC 匹配 → 直接加载。
  • 降级/升级兼容:用数据文件头 4 字节的 oldStyleActualSize 再验一次 CRC,兼容「老版本 SDK 写过又换回新版本」。
  • 回退到 lastConfirmed:用 m_lastConfirmedMetaInfo 的 (lastActualSize, lastCRCDigest) 验证 → 成功就 把账本改写为这个安全点,等于回滚到上次重整状态。
  • 业务决策:MMKVHandler.onMMKVCRCCheckFail / onMMKVFileLengthError 返回
    • 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 其它实用能力速查

    能力Java APICore 实现
    枚举全部 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 的源码级对比

    维度SharedPreferencesMMKV(源码依据)
    存储格式 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 区
    }

  • 没有「待写队列」这种东西。encode() 返回时,字节已经在 mmap 区里了,没有任何东西留给后台去做,因此 不存在可以被 waitToFinish() 等待的 future;
  • 主线程不触发 write()/fsync(),脏页回写由内核 writeback 线程负责,与应用线程完全解耦;
  • MMKV 里没有 onStop 钩子,也不参与组件生命周期,因此不会有「生命周期切换时集中 flush」的模式。
  • Android 8.1+ 已经改用 QueuedWork.beginDefer/finishDefer + 生命周期感知缓解 SP 的这个问题,但这属于「打补丁」;MMKV 是从数据结构上消除。
  • 需要老实说明的边界:mmap 消除的是「同步 IO 等待」,但没消除页错误。首次访问未 fault-in 的映射页会触发 minor fault;如果内核已把这些干净页回收,访问时会变成 major fault(需读盘),此时依然可能卡住主线程 —— 只是概率与时长远低于 SP 的全量 XML 重写。另外 MemoryFile::truncate 内的 zeroFillFile 与 ftruncate 是 真系统调用,发生在写路径的扩容瞬间,属于 MMKV 唯一可见的 IO 尖刺(Core/MMKV_IO.cpp:443)。

    Q3:MMKV 的多进程是怎么实现的?为什么不用 ContentProvider?

    答(四层递进):

  • 共享载体:所有进程 mmap 同一个磁盘文件(Core/MMKV.cpp:88,路径由 mappedKVPathWithID 决定)。因为用的是 MAP_SHARED,各进程看到的是同一批 page cache 物理页 —— 数据可见性由内核白送,不需要任何 IPC。
  • 互斥:Core/MMKV.cpp:93 m_fileLock(new FileLock(m_metaFile->getFd())),读写分别用 LOCK_SH/LOCK_EX(Core/InterProcessLock.cpp:85-126)。锁加在 .crc 文件的 fd 上,因为那个 fd 全程稳定,而数据文件会被反复 munmap/mmap。
  • 一致性协议(核心):Core/MMKV_IO.cpp:305 的 checkLoadData(),在 每次读写入口 比对 meta 里的 (m_sequence, m_crcDigest, actualSize/fileSize) 三元组,区分三种变更:
    • crcDigest 变 + 文件大小不变 → partialLoadFromFile() 增量 CRC + 只解新增段 + 写指针跟进(Core/MMKV_IO.cpp:166-177);
    • crcDigest 变 + 文件大小变 → 全量重载;
    • sequence 变 → 别的进程做了 fullWriteback,所有 offset 失效,必须全量重载。
  • 为什么不用 ContentProvider:ContentProvider 是 C/S 架构,读写要经过 Binder 一次同步事务 + Provider 进程必须活着(冷启动拉起 = 数百 ms)。MMKV 一次 encode 只是两次 memcpy,走 Binder 的话光上下文切换就慢两个数量级。而且 ContentProvider 无法提供「多个进程直接看到同一块内存」的能力,仍然要一次数据搬运。
  • flock 的 robust 性:flock 锁在 open file description 上,进程被 kill -9 时内核关 fd 自动放锁,不会留下永久死锁(fcntl 记录锁在进程/线程与 fd 关系上语义更复杂,容易误释放)。唯一例外是 ashmem,退化成 fcntl(Core/InterProcessLock_Android.cpp:47)。
  • 【纠偏】三个必须澄清的点

  • 「写指针同步更新到 mmap 内存中的全局指针」不准确。 全局可见的「写指针」其实在 .crc 元文件的 m_actualSize 字段,不是数据文件里的一个变量。数据文件头那 4 字节 actualSize 只是降级兼容(Core/MMKV_IO.cpp:473 oldStyleWriteActualSize),权威值在 meta。
  • 库里确实有 MMKVContentProvider.java,但它与多进程同步无关。 它只为 mmkvWithAshmemID() 的 跨应用共享 传一个 ashmem fd(配 ParcelableMMKV,MMKV.java:545-546)。说「MMKV 不用 ContentProvider」时要限定范围:数据同步路径不用。
  • 「多进程模式下支持 override 原地覆盖」是错的。 Core/MMKV_IO.cpp:691 写死了 bool onlyOneKey = !m_isInterProcess && m_dic->size() == 1;,多进程一律走 append。
  • 加分点: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.onExit()(Core/MMKV.cpp:262,对应 Java MMKV.java:317 的 public static native void onExit())—— 通常 App 退出时调一次;
  • MMKV.clearMemoryCache(mmapID) / 实例版(Core/MMKV.cpp:302)—— 业务显式调用;
  • MemoryFile::truncate() 内部的临时 munmap(Core/MemoryFile.cpp:173)—— 为重新映射,不是释放。
  • 那 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)。

    【纠偏】「写时复制:修改时创建新内存页,避免写入崩溃损坏原有数据」—— 不成立

  • MemoryFile.cpp:197 用的是 MAP_SHARED,COW 对应的是 MAP_PRIVATE。用 MAP_PRIVATE 的话写入根本不落盘,多进程也不共享 —— 与 MMKV 的立身之本直接矛盾。
  • 全库没有任何「为修改而分配新页/新文件」的逻辑。写入就是往 m_output 当前位置 memcpy。
  • 真实的抗崩溃机制是:①账本页原子写 + ②CRC 校验 + ③lastConfirmed 回滚点 + ④贪心解码。它允许「最后几条记录可能丢」,但保证「不会读到垃圾、不会整体损坏」。
  • 唯一沾边「复制」的地方是 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) 扩容

  • 垃圾的产生:每次更新/删除都追加一条记录(Core/MMKV_IO.cpp:822),旧记录留在原地;m_hasFullWriteback 被置 false(:~756 附近,函数尾部)。
  • 触发回收:ensureMemorySize() 发现 newSize >= m_output->spaceLeft()(Core/MMKV_IO.cpp:408)→ prepareEncode + expandAndWriteBack → doFullWriteBack → memmoveDictionary(Core/MMKV_IO.cpp:1104)按 offset 排序、合并相邻段、memmove 到头部,只搬有效数据。
  • 不重新序列化的关键:prepareEncode(Core/MMKV_IO.cpp:350)用 holder 里的 computedKVSize 纯算术算出 totalSize,assert(writtenSize == totalSize)(:1154)自证。重整的 CPU 成本是 memmove 而不是 encode。
  • 扩容判据带「未来预留」:futureUsage = avgItemSize * max(8, laterDicCount/2),do { fileSize *= 2; } while (lenNeeded + futureUsage >= fileSize)(Core/MMKV_IO.cpp:430-437)。所以增长是 指数摊还 的,不会每写几条就 ftruncate 一次。
  • 上限:MMKV 单文件默认初始 DEFAULT_MMAP_SIZE(= 1 页,4KB),可经 mmkvWithID(…, expectedCapacity)(MMKV.java:379)预分配避开首轮多次扩容。
  • 缩容需显式调用:trim()(Core/MMKV_IO.cpp:1413)fullWriteback() 之后 while (fileSize > (m_actualSize + Fixed32Size) * 2) fileSize /= 2,再 truncate。
  • 一句话答:逻辑空间会自动回收(重整),磁盘占用会自动摊还增长但不会自动缩回;要归还磁盘必须调 trim()。

    顺带纠正一个说法:「仅在空间达到阈值时才触发重整」的"阈值"具体就是 newSize >= spaceLeft()(MMKV_IO.cpp:408),是"当前剩余可追加空间不足",而不是"文件超过某个大小"。另外 clearAll()(:1457)里 keepSpace=false 时会 truncate(m_expectedCapacity),这是另一个把空间还回去的路径。

    Q7:MMKV 支持哪些数据类型?如何存自定义对象?存 Serializable 行不行?

    Java 层 encode/decode 家族(MMKV.java:713 起):

    类型encodedecode
    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 的运行时搬运,因此:

  • 依赖 SP 已经把 XML 解析完:getAll() 要求 SP 加载完毕,否则返回空 map → 静默迁移 0 条。正确做法是先 sp.getAll() 触发加载或确保 SP 已完成 loadFromDisk。
  • 只支持 7 种类型(Set 走 instanceof Set)。SP 里理论上也只有这几种,但 unknown type 只写日志、不报错,返回值仍是 kvs.size() —— 返回值不能作为"成功迁移 N 条"的证据,这是个真实的坑。
  • 每条独立 encode,会触发 N 次 meta 写。几百条无所谓,上万条建议迁移完 trim() 或直接 MMKV.clearAll() 前一次性做完。
  • 不是幂等的:调两次会搬两次(内容相同,配合 compareBeforeSet 可省 IO,但默认关闭)。
  • 原 SP 文件不会被删除,也不会自动接管后续写入。真正的迁移方案还得配合「读写代理切换 + 一次性标记位」。
  • 工程上推荐的标准迁移三段式:

    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 条,够应付任何追问:

  • 两个文件:数据文件(追加式 protobuf 记录流)+ .crc 元文件(一页以内的账本:actualSize / crcDigest / sequence / IV / flags)。
  • 一份索引:unordered_map<key, {keySize, valueSize, offset}>,读值 = 指针算术 + 零拷贝视图。
  • 三层锁:进程内 ThreadLock、跨进程 flock(shared 读 / exclusive 写,加在 .crc 的 fd 上,进程死即锁释放)。
  • 一套增量同步协议:读写前比 checkLoadData() 的三元组;能增量则 partialLoadFromFile(增量 CRC + 只解新段 + 写指针跟进),不能则全量重来。
  • 一条自愈链:CRC 校验 → 降级兼容 → lastConfirmed 回滚 → 业务策略 → 贪心解码抢救 → 立即重整固化。

  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » MMKV 源码级剖析(基于 v1.3.16 真实源码)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!