MMKV - 高效的本地 Key-Value 存储

mmap

mmap 在进程的虚拟地址空间开辟一块区域,这块区域映射文件在磁盘上的物理地址,是将内存地址空间映射到磁盘地址空间的一种方法

读/写操作访问虚拟地址空间这一段映射地址,通过查询页表发现这一段地址并不在物理页面上(因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中),因此引发缺页异常,内核发起请求调页过程。调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用 nopage 函数把所缺的页从磁盘装入到主存中。

之后如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程;修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用 msync() 来强制同步, 这样所写的内容就能立即保存到文件里了

它的优点有:

  1. 常规文件操作需要从磁盘到页缓存再到用户主存的 两次数据拷贝,而 mmap 操控文件只需要从磁盘到用户主存的 一次数据拷贝 过程

  2. 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

  3. 提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。如果进程 A 和进程 B 都映射了区域 C,当 A 第一次读取 C 时通过缺页从磁盘复制文件页到内存中;但当 B 再读 C 的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

  4. 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件 I/O 操作,极大影响效率。这个问题可以通过 mmap 映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap 都可以发挥其功效。

MemoryFile

MMKV 使用 MemoryFile 包装 mmap 相关逻辑

使用 mmap 需要注意的一个关键点是,mmap 映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)。原因是内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap 从磁盘到虚拟地址空间的映射也必须是页。

class MemoryFile {
    File m_diskFile;    // 磁盘上的文件
    void *m_ptr;        // 映射到文件物理地址的区域(在内存地址空间里),它的起始地址
    size_t m_size;      // 内存空间区域的大小
}

using MMKVFileHandle_t = HANDLE;
using MMKVPath_t = std::wstring;

class File {
    MMKVPath_t m_path;        // 文件路径
    MMKVFileHandle_t m_fd;    // 打开的文件描述符
}

// 通过系统调用 open 打开文件拿到文件描述符 fd,并用系统调用 fstat 拿到文件大小,然后 mmap 这整个文件获得映射区域的内存地址

using MMKVPath_t = std::wstring;

MemoryFile::MemoryFile(MMKVPath_t path) : m_diskFile(std::move(path), OpenFlag::ReadWrite | OpenFlag::Create), m_ptr(nullptr), m_size(0) {
    reloadFromFile();
}

void MemoryFile::reloadFromFile() {
    if (!m_diskFile.open()) {
        MMKVError("fail to open:%s, %s", m_diskFile.m_path.c_str(), strerror(errno));
    } else {
        FileLock fileLock(m_diskFile.m_fd);
        InterProcessLock lock(&fileLock, ExclusiveLockType);
        SCOPED_LOCK(&lock);

        mmkv::getFileSize(m_diskFile.m_fd, m_size);
        if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {    // 确保文件大小是内存页大小的整数倍
            size_t roundSize = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
            truncate(roundSize);
        } else {
            auto ret = mmap();
            if (!ret) {
                doCleanMemoryCache(true);
            }
        }
    }
}

bool File::open() {
    if (isFileValid()) {
        return true;
    }
    m_fd = ::open(m_path.c_str(), OpenFlag2NativeFlag(m_flag), S_IRWXU);
    if (!isFileValid()) {
        MMKVError("fail to open [%s], %d(%s)", m_path.c_str(), errno, strerror(errno));
        return false;
    }
    MMKVInfo("open fd[%p], %s", m_fd, m_path.c_str());
    return true;
}

bool getFileSize(int fd, size_t &size) {
    struct stat st = {};
    if (fstat(fd, &st) != -1) {
        size = (size_t) st.st_size;
        return true;
    }
    return false;
}

bool MemoryFile::mmap() {
    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;
    }

    return true;
}

Encoding - 数据格式

深入 SharedPreferences:架构、缺点和优化 研究过 SharedPreferences 本质上是内存中的 HashMap 和磁盘上的 XML 文件,Java HashMap 提供了 CURD Api,持久化时序列化为 XML 格式,本质上是以字符串存储

但在 MMKV 里面对的是一整块内存区域,怎么对这块区域进行 CURD 操作呢?怎么实现 Key-Value Mapping 呢?

在内存空间里,MMKV 使用 std::unordered_map 这一数据结构实现 Key-Value Mapping,而 Key-Value 对的内容则是以一种很紧凑的格式存储在 mmap 开辟的内存区域

[(key-size)(key-data)(value-size)(value-data)][(key-size)(key-data)(value-size)(value-data)]...

因为 mmap 内存区域是对文件物理地址的映射,所以持久化在磁盘上的格式也是上面这种紧凑格式

Varints - 变长的 size

上面的 key-sizevalue-size 分别表示 key 和 value 的长度,这两个字段所占大小是可变的,具体规则如下:

  1. 先读取一个字节(8 bits),如果 > 0(第一个 bit 是 0)则余下 7 bits 就表示其值,返回

  2. 否则说明余下 7 bits 不能完整地表示值,把这 7 bits 作为低 7 位存储在 result,继续找值的高位

  3. 读取一个字节,如果 > 0 则余下 7 bits 作为 result 的高 7 位,与上面获得的低 7 位共 14 bits 就是值,返回

  4. 否则又继续找下 7 bits…

  5. 也就是每次读取一个字节,第一个 bit 表示需不需要读取下一个字节(0 - 不需要,1 - 需要),剩下 7 bits 构成了值的一部分

下面的 pbRawVarint32SizereadRawVarint32 就是在计算变长的 size 字段到底有几个字节的长度,上面这个算法是参考 Protocol Buffers - Base 128 Varints

Put - 写操作

public class MMKV implements SharedPreferences, SharedPreferences.Editor {

    @Override
    public Editor putString(String key, @Nullable String value) {
        encodeString(nativeHandle, key, value);
        return this;
    }

    private native boolean encodeString(long handle, String key, @Nullable String value);    
}

MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {
    MMKV *kv = reinterpret_cast<MMKV *>(handle);
    if (kv && oKey) {
        string key = jstring2string(env, oKey);
        if (oValue) {
            string value = jstring2string(env, oValue);
            return (jboolean) kv->set(value, key);
        } else {
            kv->removeValueForKey(key);
            return (jboolean) true;
        }
    }
    return (jboolean) false;
}

bool MMKV::set(const string &value, MMKVKey_t key) {    // using MMKVKey_t = const std::string &;
    if (isKeyEmpty(key)) {
        return false;
    }
    return setDataForKey(MMBuffer((void *) value.data(), value.length(), MMBufferNoCopy), key, true);
}

bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key, bool isDataHolder) {
    if ((!isDataHolder && data.length() == 0) || isKeyEmpty(key)) {
        return false;
    }
    SCOPED_LOCK(m_lock);
    SCOPED_LOCK(m_exclusiveProcessLock);
    checkLoadData();
    {
        auto itr = m_dic->find(key);    // using MMKVMap = std::unordered_map<std::string, mmkv::KeyValueHolder>;
        if (itr != m_dic->end()) {      // mmkv::MMKVMap *m_dic;
            auto ret = appendDataWithKey(data, itr->second, isDataHolder);
            if (!ret.first) {
                return false;
            }
            itr->second = std::move(ret.second);
        } else {                        // key 不存在的情况
            auto ret = appendDataWithKey(data, key, isDataHolder);
            if (!ret.first) {
                return false;
            }
            m_dic->emplace(key, std::move(ret.second));
        }
    }
    m_hasFullWriteback = false;
    return true;
}

KVHolderRet_t MMKV::appendDataWithKey(const MMBuffer &data, MMKVKey_t key, bool isDataHolder) {    // key 不存在的情况
    auto keyData = MMBuffer((void *) key.data(), key.size(), MMBufferNoCopy);
    return doAppendDataWithKey(data, keyData, isDataHolder, static_cast<uint32_t>(keyData.length()));
}

class MMBuffer {    // MMBuffer 代表一块内存区域
    size_t size;    // 内存区域的大小
    void *ptr;      // 内存区域的起始地址
}

// 写操作 isDataHolder 总是 true,isDataHolder 为 false 表示删除 key,后面会讲到
KVHolderRet_t
MMKV::doAppendDataWithKey(const MMBuffer &data, const MMBuffer &keyData, bool isDataHolder, uint32_t originKeyLength) {
    auto isKeyEncoded = (originKeyLength < keyData.length());    // 对于 key 不存在的情况,isKeyEncoded == false
    auto keyLength = static_cast<uint32_t>(keyData.length());
    auto valueLength = static_cast<uint32_t>(data.length());
    if (isDataHolder) {
        valueLength += pbRawVarint32Size(valueLength);
    }
    // size needed to encode the key
    size_t size = isKeyEncoded ? keyLength : (keyLength + pbRawVarint32Size(keyLength));
    // size needed to encode the value
    size += valueLength + pbRawVarint32Size(valueLength);
    SCOPED_LOCK(m_exclusiveProcessLock);

    bool hasEnoughSize = ensureMemorySize(size);
    if (!hasEnoughSize || !isFileValid()) {
        return make_pair(false, KeyValueHolder());
    }

    // writeRawData 写入数据内容,writeData 先写入数据长度再写入数据内容
    // 对于 key 不存在的情况,isKeyEncoded == false,isDataHolder == true
    // 那么写入的顺序是:写入 key 长度,写入 key 内容,写入 value 长度,写入 value 内容
    // 正如上面数据格式里描述的一样
    try {
        if (isKeyEncoded) {
            m_output->writeRawData(keyData);
        } else {
            m_output->writeData(keyData);
        }
        if (isDataHolder) {
            m_output->writeRawVarint32((int32_t) valueLength);
        }
        m_output->writeData(data); // note: write size of data
    } catch (std::exception &e) {
        MMKVError("%s", e.what());
        return make_pair(false, KeyValueHolder());
    }

    // 返回一个 pair,first 表示操作成功 or 失败
    // second 记录了这个 Key-Value 对在内存和文件的偏移量,以及 key 和 value 的大小
    // 这样就能够通过 key 找到 KeyValueHolder 并快速地定位出 value 所在内存地址
    auto offset = static_cast<uint32_t>(m_actualSize);
    auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;
    m_actualSize += size;
    updateCRCDigest(ptr, size);
    return make_pair(true, KeyValueHolder(originKeyLength, valueLength, offset));
}

struct KeyValueHolder {
    uint16_t keySize;      // key 的大小
    uint32_t valueSize;    // value 的大小
    uint32_t offset;       // 相对于 mmap 内存区域的偏移量,也是这个 Key-Value 对在文件内的偏移量(这样才能从文件恢复)
};

void CodedOutputData::writeData(const MMBuffer &value) {
    this->writeRawVarint32((int32_t) value.length());
    this->writeRawData(value);
}

void CodedOutputData::writeRawData(const MMBuffer &data) {
    size_t numberOfBytes = data.length();
    if (m_position + numberOfBytes > m_size) {
        auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) +
                   ", m_size: " + to_string(m_size);
        throw out_of_range(msg);
    }
    memcpy(m_ptr + m_position, data.getPtr(), numberOfBytes);
    m_position += numberOfBytes;
}

Update - 修改的情况

在上面的基本流程里,如果 key 存在则进行修改操作,但此时存在一个问题,Key-Value 对是紧凑地排列在一块连续的内存区域上的,如果改变某个 value 的长度那后面的内容对应地需要整体前移或后移,这样修改的代价就会变得很大

所以 MMKV 采取一个用空间换时间的策略:不修改原有的 Key-Value 对,而是将新的 Key-Value 对附加(append)到末尾(当然此时 key 是一样的)

// 修改/更新的情况下,从词典里找到已存在的键值对 KeyValueHolder
// 新增的情况,第二个参数是 MMKVKey_t key
KVHolderRet_t MMKV::appendDataWithKey(const MMBuffer &data, const KeyValueHolder &kvHolder, bool isDataHolder) {
    SCOPED_LOCK(m_exclusiveProcessLock);

    uint32_t keyLength = kvHolder.keySize;
    // size needed to encode the key
    size_t rawKeySize = keyLength + pbRawVarint32Size(keyLength);

    // ensureMemorySize() might change kvHolder.offset, so have to do it early
    {
        auto valueLength = static_cast<uint32_t>(data.length());
        if (isDataHolder) {
            valueLength += pbRawVarint32Size(valueLength);
        }
        auto size = rawKeySize + valueLength + pbRawVarint32Size(valueLength);
        bool hasEnoughSize = ensureMemorySize(size);
        if (!hasEnoughSize) {
            return make_pair(false, KeyValueHolder());
        }
    }

    // 跟新增的情况一样进入 doAppendDataWithKey,但有所不同
    // keyLength 指的是 key data size,跟新增时一样
    // 第二个参数 MMBuffer keyData 它指向 key 在 mmap 内存区域的起始地址,size 是整个 key entry 的长度(包含 key data size 部分)
    auto basePtr = (uint8_t *) m_file->getMemory() + Fixed32Size;
    MMBuffer keyData(basePtr + kvHolder.offset, rawKeySize, MMBufferNoCopy);
    return doAppendDataWithKey(data, keyData, isDataHolder, keyLength);
}

// 修改时 isDataHolder 也是 true
KVHolderRet_t MMKV::doAppendDataWithKey(const MMBuffer &data, const MMBuffer &keyData, bool isDataHolder, uint32_t originKeyLength) {
    auto isKeyEncoded = (originKeyLength < keyData.length());    // 修改时 isKeyEncoded == true,原因是 keyData 包含了 key data size 部分
    auto keyLength = static_cast<uint32_t>(keyData.length());
    auto valueLength = static_cast<uint32_t>(data.length());
    if (isDataHolder) {
        valueLength += pbRawVarint32Size(valueLength);
    }
    // size needed to encode the key
    size_t size = isKeyEncoded ? keyLength : (keyLength + pbRawVarint32Size(keyLength));
    // size needed to encode the value
    size += valueLength + pbRawVarint32Size(valueLength);
    SCOPED_LOCK(m_exclusiveProcessLock);

    bool hasEnoughSize = ensureMemorySize(size);
    if (!hasEnoughSize || !isFileValid()) {
        return make_pair(false, KeyValueHolder());
    }

    // 这里可以看到修改已有的 Key-Value 并不会真正地去 mmap 内存区域里做修改操作,
    // 而是将修改后的 Key-Value append 至 mmap 内存区域尾部,也即是 append 到磁盘文件尾部
    // isKeyEncoded == true,用 writeRawData 将 keyData 对应的整块内存写入,keyData 是从 dic 里查找出来的,包含完整的 key data size 和 key data
    // 然后写 value data size 和 value data
    try {
        if (isKeyEncoded) {
            m_output->writeRawData(keyData);
        } else {
            m_output->writeData(keyData);
        }
        if (isDataHolder) {
            m_output->writeRawVarint32((int32_t) valueLength);
        }
        m_output->writeData(data); // note: write size of data
    } catch (std::exception &e) {
        MMKVError("%s", e.what());
        return make_pair(false, KeyValueHolder());
    }
    auto offset = static_cast<uint32_t>(m_actualSize);
    auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;
    m_actualSize += size;
    updateCRCDigest(ptr, size);

    // 返回一个新的 KeyValueHolder 实例,指向刚刚 append 的 Key-Value 对在 mmap 内存区域的地址,但 key 部分跟旧的是一样的
    return make_pair(true, KeyValueHolder(originKeyLength, valueLength, offset));
}

bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key, bool isDataHolder) {
    if ((!isDataHolder && data.length() == 0) || isKeyEmpty(key)) {
        return false;
    }
    SCOPED_LOCK(m_lock);
    SCOPED_LOCK(m_exclusiveProcessLock);
    checkLoadData();
    {
        auto itr = m_dic->find(key);    // using MMKVMap = std::unordered_map<std::string, mmkv::KeyValueHolder>;
        if (itr != m_dic->end()) {      // mmkv::MMKVMap *m_dic;
            auto ret = appendDataWithKey(data, itr->second, isDataHolder);
            if (!ret.first) {
                return false;
            }
            itr->second = std::move(ret.second);
        } else {                        // key 不存在的情况
            auto ret = appendDataWithKey(data, key, isDataHolder);
            if (!ret.first) {
                return false;
            }
            // 更新词典,后续通过这个 key 找到的就是 append 至内存区域末尾的最新的 value 值
            m_dic->emplace(key, std::move(ret.second));
        }
    }
    m_hasFullWriteback = false;
    return true;
}

Remove - 删除操作

上面聊过由于 Key-Value 对是以下面这种紧凑的格式一个个排列在一起的,如果修改操作改变了已有的某个 Value 的长度,那么后面的数据就要整块前移或者后移

删除操作也是一样的,将某个 Key-Value 对删除后,那么后面的 Key-Value 对也要整体前移,这样删除操作的效率就会很低,所以 MMKV 把删除操作改为 append 一个相同 key 但 value data size 为 0 的新项,并从词典里移除此 key 对应的项

[(key-size)(key-data)(value-size)(value-data)][(key-size)(key-data)(value-size)(value-data)]...
public class MMKV implements SharedPreferences, SharedPreferences.Editor {

    @Override
    public Editor remove(String key) {
        removeValueForKey(key);
        return this;
    }

    public void removeValueForKey(String key) {
        removeValueForKey(nativeHandle, key);
    }

    private native void removeValueForKey(long handle, String key);  
}

MMKV_EXPORT void removeValueForKey(void *handle, char *oKey) {
    MMKV *kv = static_cast<MMKV *>(handle);
    if (kv && oKey) {
        string key(oKey);
        kv->removeValueForKey(key);
    }
}

void MMKV::removeValueForKey(MMKVKey_t key) {    // using MMKVKey_t = const std::string &;
    if (isKeyEmpty(key)) {
        return;
    }
    SCOPED_LOCK(m_lock);
    SCOPED_LOCK(m_exclusiveProcessLock);
    checkLoadData();

    removeDataForKey(key);
}

bool MMKV::removeDataForKey(MMKVKey_t key) {
    if (isKeyEmpty(key)) {
        return false;
    }
    {
        auto itr = m_dic->find(key);
        if (itr != m_dic->end()) {
            m_hasFullWriteback = false;
            static MMBuffer nan;      // size == 0,ptr == null
            auto ret = appendDataWithKey(nan, itr->second);
            if (ret.first) {
                m_dic->erase(itr);    // 对于 mmap 内存区域是 append 一个 empty value 的项,对于用以 mapping 的词典是移除此项
            }
            return ret.first;
        }
    }
    return false;
}

// 删除操作下 isDataHolder == false,data.size == 0,data.ptr == null
KVHolderRet_t MMKV::appendDataWithKey(const MMBuffer &data, MMKVKey_t key, bool isDataHolder) {
    auto keyData = MMBuffer((void *) key.data(), key.size(), MMBufferNoCopy);
    return doAppendDataWithKey(data, keyData, isDataHolder, static_cast<uint32_t>(keyData.length()));
}

KVHolderRet_t
MMKV::doAppendDataWithKey(const MMBuffer &data, const MMBuffer &keyData, bool isDataHolder, uint32_t originKeyLength) {
    auto isKeyEncoded = (originKeyLength < keyData.length());    // 删除的情况下,isKeyEncoded == true
    auto keyLength = static_cast<uint32_t>(keyData.length());
    auto valueLength = static_cast<uint32_t>(data.length());
    if (isDataHolder) {
        valueLength += pbRawVarint32Size(valueLength);
    }
    // size needed to encode the key
    size_t size = isKeyEncoded ? keyLength : (keyLength + pbRawVarint32Size(keyLength));
    // size needed to encode the value
    size += valueLength + pbRawVarint32Size(valueLength);
    SCOPED_LOCK(m_exclusiveProcessLock);

    bool hasEnoughSize = ensureMemorySize(size);
    if (!hasEnoughSize || !isFileValid()) {
        return make_pair(false, KeyValueHolder());
    }

    // writeRawData 写入数据内容,writeData 先写入数据长度再写入数据内容
    // 在删除操作里,isKeyEncoded == true,isDataHolder == false
    // key 包含 size 和 data,原封不动写入
    // value 写入 size 0,没有 data
    try {
        if (isKeyEncoded) {
            m_output->writeRawData(keyData);
        } else {
            m_output->writeData(keyData);
        }
        if (isDataHolder) {
            m_output->writeRawVarint32((int32_t) valueLength);
        }
        m_output->writeData(data); // note: write size of data
    } catch (std::exception &e) {
        MMKVError("%s", e.what());
        return make_pair(false, KeyValueHolder());
    }
    auto offset = static_cast<uint32_t>(m_actualSize);
    auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;
    m_actualSize += size;
    updateCRCDigest(ptr, size);
    return make_pair(true, KeyValueHolder(originKeyLength, valueLength, offset));
}

Get - 查找操作

查询操作比较简单:

  1. 根据 string key 从词典里查找出 KeyValueHolder

  2. KeyValueHolder.offset 是键值对在 mmap 内存区域的偏移量,加上内存区域的起始地址即可定位出键值对在虚拟内存中的起始地址

  3. 加上 key size 和 key data 的偏移量就是 value 的内存地址

  4. 再加上 value size 的偏移量就是 value data 的内存地址

public class MMKV implements SharedPreferences, SharedPreferences.Editor {

    @Nullable
    @Override
    public String getString(String key, @Nullable String defValue) {
        return decodeString(nativeHandle, key, defValue);
    }

    @Nullable
    private native String decodeString(long handle, String key, @Nullable String defaultValue);
}

MMKV_JNI jstring decodeString(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jstring oDefaultValue) {
    MMKV *kv = reinterpret_cast<MMKV *>(handle);
    if (kv && oKey) {
        string key = jstring2string(env, oKey);
        string value;
        bool hasValue = kv->getString(key, value);
        if (hasValue) {
            return string2jstring(env, value);
        }
    }
    return oDefaultValue;
}

bool MMKV::getString(MMKVKey_t key, string &result) {           // using MMKVKey_t = const std::string &;
    if (isKeyEmpty(key)) {
        return false;
    }
    SCOPED_LOCK(m_lock);
    auto data = getDataForKey(key);
    if (data.length() > 0) {
        try {
            CodedInputData input(data.getPtr(), data.length());  // 包含了 value size 和 value data
            result = input.readString();
            return true;
        } catch (std::exception &exception) {
            MMKVError("%s", exception.what());
        }
    }
    return false;
}

MMBuffer MMKV::getDataForKey(MMKVKey_t key) {
    checkLoadData();
    {
        auto itr = m_dic->find(key);
        if (itr != m_dic->end()) {
            auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;  // mmap 内存区域的起始地址
            return itr->second.toMMBuffer(basePtr);  // 加上 offset 就是键值对的内存地址,加上 key size 和 key value 偏移量则是 value 的内存地址                          
        }
    }
    MMBuffer nan;
    return nan;
}

string CodedInputData::readString() {
    int32_t size = readRawVarint32();                         // 计算变长的 size 值,在上面的章节中有讲过计算过程
    if (size < 0) {
        throw length_error("InvalidProtocolBuffer negativeSize");
    }

    auto s_size = static_cast<size_t>(size);
    if (s_size <= m_size - m_position) {
        string result((char *) (m_ptr + m_position), s_size);  // 加上 value size 的偏移量就是 value data,作为 string 返回
        m_position += s_size;
        return result;
    } else {
        throw out_of_range("InvalidProtocolBuffer truncatedMessage");
    }
}

// 在章节 [Varints - 变长的 size] 里讲过,一个字节里,高 1 位表示是否需要读取下一个字节来解析,低 7 位存储数值
int32_t CodedInputData::readRawVarint32() {
    int8_t tmp = this->readRawByte();
    if (tmp >= 0) {
        return tmp;
    }
    int32_t result = tmp & 0x7f;
    if ((tmp = this->readRawByte()) >= 0) {
        result |= tmp << 7;
    } else {
        result |= (tmp & 0x7f) << 7;
        if ((tmp = this->readRawByte()) >= 0) {
            result |= tmp << 14;
        } else {
            result |= (tmp & 0x7f) << 14;
            if ((tmp = this->readRawByte()) >= 0) {
                result |= tmp << 21;
            } else {
                result |= (tmp & 0x7f) << 21;
                result |= (tmp = this->readRawByte()) << 28;
                if (tmp < 0) {
                    // discard upper 32 bits
                    for (int i = 0; i < 5; i++) {
                        if (this->readRawByte() >= 0) {
                            return result;
                        }
                    }
                    throw invalid_argument("InvalidProtocolBuffer malformed varint32");
                }
            }
        }
    }
    return result;
}

loadFromFile - 构建索引词典

MMKV 依靠 std::unordered_map 词典数据结构提供 Key-Value 映射和查找服务,但这个词典结构只存在于内存里并没有实现持久化,所以当第一次打开某个 MMKV 数据文件时,需要将文件从头到尾扫描解析一遍,以构建和还原整个词典数据结构

从下面的结构可以看出,reloadFromFile 的过程并不需要将 mmap 映射内存区域的所有页都加载进内存,只需要根据 key size 和 value size 跳跃式地访问内存页即可,所需信息包括:

  1. offset - 键值对在 mmap 映射文件里的偏移量
  2. keySize - key 大小,它是一个变长的数值
  3. keyData - 作为 string 用以查找
  4. valueSize - value 的大小
  5. valueData 在被访问前是不需要加载进内存的,所以会有大量的虚拟页无需分配
struct KeyValueHolder {
    uint16_t computedKVSize; // internal use only
    uint16_t keySize;
    uint32_t valueSize;
    uint32_t offset;
};

using MMKVMap = std::unordered_map<std::string, mmkv::KeyValueHolder>;

class MMKV {
    mmkv::MMKVMap *m_dic;
}

与 SharedPreferences 的区别:

  1. SharedPreferences 在构造函数里起个工作线程执行文件解析工作,在解析工作完成前,任何的读写操作都会被阻塞;而 MMKV 则是在构造函数里(当前线程)立即执行文件解析任务,任务完成(无论成功 or 失败)退出构造函数才能执行读写操作

  2. MMKV 的解析速度要快很多,因为 SharedPreferences 是将 XML 文件全量加载和解析至内存,MMKV 就如上一章节描述的那样并不会立即加载 value data,只加载了 key size、key data 和 value size 三个部分,这三个部分一般情况下要比 value data 小很多

上面介绍过所有的 CURD 操作都是 append 一个键值对到文件末尾,而从文件构建 mapping 词典时,是从文件头扫描到文件尾的,所以旧的键值对会被新 append 的键值对替换/删除,看下具体流程:

public static MMKV mmkvWithID(String mmapID) throws RuntimeException {
    if (rootDir == null) {
        throw new IllegalStateException("You should Call MMKV.initialize() first.");
    }
    long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, null);
    return checkProcessMode(handle, mmapID, SINGLE_PROCESS_MODE);
}

private native static long getMMKVWithID(String mmapID, int mode, @Nullable String cryptKey, @Nullable String rootPath);

MMKV_EXPORT void *getMMKVWithID(const char *mmapID, int32_t mode, const char *cryptKey, const char *rootPath) {
    MMKV *kv = nullptr;
    if (!mmapID) {
        return kv;
    }
    string str = mmapID;

    bool done = false;
    if (cryptKey) {
        string crypt = cryptKey;
        if (crypt.length() > 0) {
            if (rootPath) {
                string path = rootPath;
                kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, &path);
            } else {
                kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, nullptr);
            }
            done = true;
        }
    }
    if (!done) {
        if (rootPath) {
            string path = rootPath;
            kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, &path);
        } else {
            kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, nullptr);  // 走这条路
        }
    }

    return kv;
}

MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath) {
    if (mmapID.empty()) {
        return nullptr;
    }
    SCOPED_LOCK(g_instanceLock);

    auto mmapKey = mmapedKVKey(mmapID, rootPath);  // id 对应的 MMKV 实例如果已创建则直接返回
    auto itr = g_instanceDic->find(mmapKey);
    if (itr != g_instanceDic->end()) {
        MMKV *kv = itr->second;
        return kv;
    }
    if (rootPath) {                                // 准备所需的目录
        if (!isFileExist(*rootPath)) {
            if (!mkPath(*rootPath)) {
                return nullptr;
            }
        }
        MMKVInfo("prepare to load %s (id %s) from rootPath %s", mmapID.c_str(), mmapKey.c_str(), rootPath->c_str());
    }
    auto kv = new MMKV(mmapID, size, mode, cryptKey, rootPath);  // 看这里如何创建 MMKV 实例
    (*g_instanceDic)[mmapKey] = kv;
    return kv;
}

// mmapID 是文件名,size 取默认值则为一个内存页的大小 4K,其余参数为 null
MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath)
    : m_mmapID((mode & MMKV_BACKUP) ? mmapID : mmapedKVKey(mmapID, rootPath)) // historically Android mistakenly use mmapKey as mmapID
    , m_path(mappedKVPathWithID(m_mmapID, mode, rootPath))    // mmap 文件路径
    , m_crcPath(crcPathWithID(m_mmapID, mode, rootPath))
    , m_dic(nullptr)                                          // 用以实现 Key-Value 映射的词典,后续会初始化
    , m_dicCrypt(nullptr)
    , m_file(new MemoryFile(m_path, size, (mode & MMKV_ASHMEM) ? MMFILE_TYPE_ASHMEM : MMFILE_TYPE_FILE))  // mmap 内存区域(MemoryFile 上面介绍过,就是一块内存区域)
    , m_metaFile(new MemoryFile(m_crcPath, DEFAULT_MMAP_SIZE, m_file->m_fileType))
    , m_metaInfo(new MMKVMetaInfo())
    , m_crypter(nullptr)
    , m_lock(new ThreadLock())
    , m_fileLock(new FileLock(m_metaFile->getFd(), (mode & MMKV_ASHMEM)))
    , m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
    , m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
    , m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0 || (mode & CONTEXT_MODE_MULTI_PROCESS) != 0) {
    m_actualSize = 0;
    m_output = nullptr;

    // force use fcntl(), otherwise will conflict with MemoryFile::reloadFromFile()
    m_fileModeLock = new FileLock(m_file->getFd(), true);
    m_sharedProcessModeLock = new InterProcessLock(m_fileModeLock, SharedLockType);
    m_exclusiveProcessModeLock = nullptr;

    {
        m_dic = new MMKVMap();
    }

    m_needLoadFromFile = true;
    m_hasFullWriteback = false;

    m_crcDigest = 0;

    m_sharedProcessLock->m_enable = m_isInterProcess;
    m_exclusiveProcessLock->m_enable = m_isInterProcess;

    // sensitive zone
    {
        SCOPED_LOCK(m_sharedProcessLock);
        loadFromFile();
    }
}

void MMKV::loadFromFile() {
    if (!m_file->isFileValid()) {  
        m_file->reloadFromFile();  // m_file 是 MemoryFile,上面介绍过,这里主要是进行 open file 和 mmap 映射内存区域
    }

    if (!m_file->isFileValid()) {
        MMKVError("file [%s] not valid", m_path.c_str());
    } else {
        bool loadFromFile = false, needFullWriteback = false;
        checkDataValid(loadFromFile, needFullWriteback);

        auto ptr = (uint8_t *) m_file->getMemory();  // mmap 映射内存区域地址
        if (loadFromFile && m_actualSize > 0) {
            MMBuffer inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
            if (m_crypter) {
                clearDictionary(m_dicCrypt);
            } else {
                clearDictionary(m_dic);
            }
            if (needFullWriteback) {
                MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer);
            } else {
                MiniPBCoder::decodeMap(*m_dic, inputBuffer);    // 解析 mmap 文件,构建出词典结构以供后续查找使用
            }
            m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
            m_output->seek(m_actualSize);    // m_output 指向文件末尾,用以 append 键值对
            if (needFullWriteback) {
                fullWriteback();
            }
        } else {
            // file not valid or empty, discard everything
            SCOPED_LOCK(m_exclusiveProcessLock);

            m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
            if (m_actualSize > 0) {
                writeActualSize(0, 0, nullptr, IncreaseSequence);
                sync(MMKV_SYNC);
            } else {
                writeActualSize(0, 0, nullptr, KeepSequence);
            }
        }
        auto count = m_crypter ? m_dicCrypt->size() : m_dic->size();
        MMKVInfo("loaded [%s] with %zu key-values", m_mmapID.c_str(), count);
    }

    m_needLoadFromFile = false;
}

void MiniPBCoder::decodeMap(MMKVMap &dic, const MMBuffer &oData, size_t position) {  // position 默认为 0
    MiniPBCoder oCoder(&oData);
    oCoder.decodeOneMap(dic, position, false);
}

void MiniPBCoder::decodeOneMap(MMKVMap &dic, size_t position, bool greedy) {  // position == 0,greedy == false
    auto block = [position, this](MMKVMap &dictionary) {
        if (position) {
            m_inputData->seek(position);
        } else {
            m_inputData->readInt32();    // 文件头 32 bits ?
        }
        while (!m_inputData->isAtEnd()) {                         // 因为是从文件头扫描到文件尾,旧的键值对就会被新 append 的键值对替换 or 删除
            KeyValueHolder kvHolder;
            const auto &key = m_inputData->readString(kvHolder);  // 读取 key 的内容为 string
            if (key.length() > 0) {
                m_inputData->readData(kvHolder);                  // 读取 value size(没有读取 value data 哦)
                if (kvHolder.valueSize > 0) {                     // value size > 0 说明是正常的键值对,添加到词典里
                    dictionary[key] = move(kvHolder);
                } else {
                    auto itr = dictionary.find(key);              // [Remove - 删除操作] 里介绍过删除就是 append 一个 data size 为 0 的键值对
                    if (itr != dictionary.end()) {                // 所以这里遇到 data size == 0 就表示要从词典里移除这个 key
                        dictionary.erase(itr);
                    }
                }
            }
        }
    };

    if (greedy) {
        try {
            block(dic);
        } catch (std::exception &exception) {
            MMKVError("%s", exception.what());
        }
    } else {
        try {
            MMKVMap tmpDic;
            block(tmpDic);
            dic.swap(tmpDic);
        } catch (std::exception &exception) {
            MMKVError("%s", exception.what());
        }
    }
}

内存区域与文件的大小关系

情形一:一个文件的大小是 5000 字节,mmap 函数从一个文件的起始位置开始映射 5000 字节到虚拟内存中

分析:因为单位物理页面的大小是 4096 字节,虽然被映射的文件只有 5000 字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此 mmap 函数执行后实际映射到虚拟内存区域 8192 字节,5000~8191 的字节部分用零填充

  1. 读/写前 5000 字节(0~4999)会返回操作文件内容
  2. 读字节 5000-8191 时结果全为 0,写 5000-8191 时进程不会报错,但是所写的内容不会写入原文件中
  3. 读/写 8192 以外的磁盘部分会返回一个 SIGSECV 错误

情形二:一个文件的大小是 5000 字节,mmap 函数从一个文件的起始位置开始映射 15000 字节到虚拟内存中,即映射大小超过了原始文件的大小

分析:由于文件的大小是 5000 字节,和情形一一样其对应的两个物理页。那么这两个物理页都是合法可以读写的,只是超出 5000 的部分不会体现在原文件中。由于程序要求映射 15000 字节而文件只占两个物理页,因此 8192~15000 都不能读写,操作时会返回异常

  1. 进程可以正常读/写被映射的前 5000 字节,写操作的改动会在一定时间后反映在原文件中
  2. 对于 5000~8191 字节,进程可以进行读写过程不会报错。但是内容在写入前均为 0,另外写入后不会反映在文件中
  3. 对于 8192~14999 字节进程不能对其进行读写,会报 SIGBUS 错误
  4. 对于 15000 以外的字节进程不能对其读写,会引发 SIGSEGV 错误

情形三:一个文件初始大小为 0,使用 mmap 操作映射了 1000*4K 的大小,即 1000 个物理页大约 4M 字节空间,mmap 返回指针 ptr

分析:如果在映射建立之初,就对文件进行读写操作,由于文件大小为 0,并没有合法的物理页对应,如同情形二一样会返回 SIGBUS 错误。但是如果每次操作 ptr 读写前先增加文件的大小,那么 ptr 在文件大小内部的操作就是合法的。

例如文件扩充 4096 字节,ptr 就能操作 ptr ~ [ (char)ptr + 4095] 的空间,只要文件扩充的范围在 1000 个物理页(映射范围)内 ptr 都可以对应操作相同的大小,这样方便随时扩充文件空间,随时写入文件,不造成空间浪费

总结:

  1. mmap 映射的内存地址空间肯定是内存页大小的整数倍
  2. 读写 mmap 映射的区域不能超过映射的大小和磁盘文件的大小
  3. 特殊情况:mmap 映射大小不为页大小的整数倍,系统会开辟一块页大小整数倍且满足映射大小的虚存空间,在这块空间内读写文件大小外的地址会返回 0 且不会同步到文件中

mmap 扩容

  1. 计算扩容后的大小 newSize:每次将文件大小 double 一倍,直到能够放下新增的内容
  2. 上面说过 mmap 映射的最小单位是页,所以调整 newSize 以满足页大小的整数倍
  3. ftruncate 设置文件大小为 newSize 并将新增的扇区填充为 0(键值对是紧凑地一个个排列在一起,通过头部的 size 判断是否是有效的键值对)
  4. munmap 取消映射后进行重新映射,此时 m_ptr 指向上一次系统开辟的地址空间,mmap 时告诉(建议)系统从 m_ptr 指向的地址开始进行映射

If addr is NULL, then the kernel chooses the (page-aligned) address at which to create the mapping; this is the most portable method of creating a new mapping.
If addr is not NULL, then the kernel takes it as a hint about where to place the mapping.
on Linux, the kernel will pick a nearby page boundary (but always above or equal to the value specified by /proc/sys/vm/mmap_min_addr) and attempt to create the mapping there.
If another mapping already exists there, the kernel picks a new address that may or may not depend on the hint. The address of the new mapping is returned as the result of the call.

// since we use append mode, when -[setData: forKey:] many times, space may not be enough
// try a full rewrite to make space
// size 表示 mmap 内存区域需要有至少 size 大小的剩余空间,不是说整个内存区域要调整为 size 大小
// size 一般是新 append 的键值对的大小
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())) {

        // try a full rewrite to make space
        auto fileSize = m_file->getFileSize();
        auto preparedData = m_crypter ? prepareEncode(*m_dicCrypt) : prepareEncode(*m_dic);
        auto sizeOfDic = preparedData.second;
        size_t lenNeeded = sizeOfDic + Fixed32Size + newSize;
        size_t dicCount = m_crypter ? m_dicCrypt->size() : m_dic->size();
        size_t avgItemSize = lenNeeded / std::max<size_t>(1, dicCount);
        size_t futureUsage = avgItemSize * std::max<size_t>(8, (dicCount + 1) / 2);
        // 1. no space for a full rewrite, double it
        // 2. or space is not large enough for future usage, double it to avoid frequently full rewrite
        if (lenNeeded >= fileSize || (lenNeeded + futureUsage) >= fileSize) {
            size_t oldSize = fileSize;
            do {
                fileSize *= 2;
            } while (lenNeeded + futureUsage >= fileSize);    // 计算出新的文件大小,每次 double 文件大小直到放得下新 append 的键值对

            // if we can't extend size, rollback to old state
            if (!m_file->truncate(fileSize)) {                // 真正的 mmap 扩容操作
                return false;
            }

            // check if we fail to make more space
            if (!isFileValid()) {
                MMKVWarning("[%s] file not valid", m_mmapID.c_str());
                return false;
            }
        }
        return doFullWriteBack(move(preparedData), nullptr);
    }
    return true;
}

bool MemoryFile::truncate(size_t size) {    // 将 mmap 扩容至 size 大小
    if (!m_diskFile.isFileValid()) {
        return false;
    }
    if (size == m_size) {
        return true;
    }

    auto oldSize = m_size;
    m_size = size;
    // round up to (n * pagesize)
    if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {  // 使文件大小刚好是内存页大小的整数倍,这是 mmap 的规定
        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,否则 reloadFromFile 时会加载到错误的键值对
        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;
            return false;
        }
    }

    if (m_ptr) {                                                             // 如果已经 mmap 过了则通过系统调用 munmap 取消映射
        if (munmap(m_ptr, oldSize) != 0) {
            MMKVError("fail to munmap [%s], %s", m_diskFile.m_path.c_str(), strerror(errno));
        }
    }
    auto ret = mmap();                                                       // 重新执行 mmap 映射
    if (!ret) {
        doCleanMemoryCache(true);
    }
    return ret;
}

多进程

进程锁

文件锁 int flock(int fd, int operation); 有以下操作:

  1. LOCK_SH 获取共享锁,也叫读锁,同一文件描述符同一时间可以有多个进程持有此锁;Android 平台为 F_RDLCK
  2. LOCK_EX 获取排它锁,也叫写锁,同一文件描述符同一时间只能有一个进程持有此锁;Android 平台为 F_WRLCK
  3. 一个进程同一时间只能持有一种类型的锁:共享锁 or 排它锁,后续的 flock 调用会将已持有的锁转换为当前请求的锁类型
  4. LOCK_UN 归还当前进程已获得的锁
  5. LOCK_NB 一般情况下 flock 是阻塞的,此参数设置为非阻塞请求

MMKV 实现的进程锁底层是基于文件锁 flock 的,并用两个计数器 m_sharedLockCountm_exclusiveLockCount 实现重入和读写锁逻辑:

  1. 如果 m_sharedLockCount == m_exclusiveLockCount == 0 说明进程没有获得锁,通过 flock 请求获得锁
  2. flock 取得锁后将对应的 m_sharedLockCount 或 m_exclusiveLockCount 自增
  3. 由于进程已获得锁,后续的加锁操作只需将计数器自增,解锁操作只需将计数器自减直到归零时通过 LOCK_UN 归还底层的文件锁
  4. flock 实现的读写锁的含义跟我们通常理解的读写锁含义不太一样,所以需要用上面两个计数器来实现所需的读写锁逻辑(比如排它锁/写锁实际上可以包含读锁/读操作)
class InterProcessLock {    // MMKV 使用的进程锁,底层是文件锁
    FileLock *m_fileLock;
    LockType m_lockType;

public:
    InterProcessLock(FileLock *fileLock, LockType lockType)
        : m_fileLock(fileLock), m_lockType(lockType), m_enable(true) {
        MMKV_ASSERT(m_fileLock);
    }

    bool m_enable;

    void lock() {
        if (m_enable) {
            m_fileLock->lock(m_lockType);
        }
    }

    bool try_lock(bool *tryAgain = nullptr) {
        if (m_enable) {
            return m_fileLock->try_lock(m_lockType, tryAgain);
        }
        return false;
    }

    void unlock() {
        if (m_enable) {
            m_fileLock->unlock(m_lockType);
        }
    }
};

// FileLock 是对文件锁的包装
FileLock::FileLock(MMKVFileHandle_t fd, bool isAshmem)    // using MMKVFileHandle_t = int;
    : m_fd(fd), m_sharedLockCount(0), m_exclusiveLockCount(0), m_isAshmem(isAshmem) {
    m_lockInfo.l_type = F_WRLCK;
    m_lockInfo.l_start = 0;
    m_lockInfo.l_whence = SEEK_SET;
    m_lockInfo.l_len = 0;
    m_lockInfo.l_pid = 0;
}

// 上锁逻辑 = flock + 计数器
bool FileLock::lock(LockType lockType) {
    return doLock(lockType, true);
}

bool FileLock::doLock(LockType lockType, bool wait, bool *tryAgain) {
    if (!isFileLockValid()) {
        return false;
    }
    bool unLockFirstIfNeeded = false;

    if (lockType == SharedLockType) {
        // don't want shared-lock to break any existing locks
        if (m_sharedLockCount > 0 || m_exclusiveLockCount > 0) {
            m_sharedLockCount++;
            return true;
        }
    } else {
        // don't want exclusive-lock to break existing exclusive-locks
        if (m_exclusiveLockCount > 0) {
            m_exclusiveLockCount++;
            return true;
        }
        // prevent deadlock
        if (m_sharedLockCount > 0) {
            unLockFirstIfNeeded = true;
        }
    }

    auto ret = platformLock(lockType, wait, unLockFirstIfNeeded, tryAgain);
    if (ret) {
        if (lockType == SharedLockType) {
            m_sharedLockCount++;
        } else {
            m_exclusiveLockCount++;
        }
    }
    return ret;
}

bool FileLock::platformLock(LockType lockType, bool wait, bool unLockFirstIfNeeded, bool *tryAgain) {
#    ifdef MMKV_ANDROID
    if (m_isAshmem) {
        return ashmemLock(lockType, wait, unLockFirstIfNeeded, tryAgain);
    }
#    endif
    auto realLockType = LockType2FlockType(lockType);
    auto cmd = wait ? realLockType : (realLockType | LOCK_NB);
    if (unLockFirstIfNeeded) {
        // try lock
        auto ret = flock(m_fd, realLockType | LOCK_NB);
        if (ret == 0) {
            return true;
        }
        // let's be gentleman: unlock my shared-lock to prevent deadlock
        ret = flock(m_fd, LOCK_UN);
        if (ret != 0) {
            MMKVError("fail to try unlock first fd=%d, ret=%d, error:%s", m_fd, ret, strerror(errno));
        }
    }

    auto ret = flock(m_fd, cmd);
    if (ret != 0) {
        if (tryAgain) {
            *tryAgain = (errno == EWOULDBLOCK);
        }
        if (wait) {
            MMKVError("fail to lock fd=%d, ret=%d, error:%s", m_fd, ret, strerror(errno));
        }
        // try recover my shared-lock
        if (unLockFirstIfNeeded) {
            ret = flock(m_fd, LockType2FlockType(SharedLockType));
            if (ret != 0) {
                // let's hope this never happen
                MMKVError("fail to recover shared-lock fd=%d, ret=%d, error:%s", m_fd, ret, strerror(errno));
            }
        }
        return false;
    } else {
        return true;
    }
}

static short LockType2FlockType(LockType lockType) {
    switch (lockType) {
        case SharedLockType:
            return F_RDLCK;
        case ExclusiveLockType:
            return F_WRLCK;
    }
}

// 解锁逻辑 = flock + 计数器
bool FileLock::unlock(LockType lockType) {
    if (!isFileLockValid()) {
        return false;
    }
    bool unlockToSharedLock = false;

    if (lockType == SharedLockType) {
        if (m_sharedLockCount == 0) {
            return false;
        }
        // don't want shared-lock to break any existing locks
        if (m_sharedLockCount > 1 || m_exclusiveLockCount > 0) {
            m_sharedLockCount--;
            return true;
        }
    } else {
        if (m_exclusiveLockCount == 0) {
            return false;
        }
        if (m_exclusiveLockCount > 1) {
            m_exclusiveLockCount--;
            return true;
        }
        // restore shared-lock when all exclusive-locks are done
        if (m_sharedLockCount > 0) {
            unlockToSharedLock = true;
        }
    }

    auto ret = platformUnLock(unlockToSharedLock);
    if (ret) {
        if (lockType == SharedLockType) {
            m_sharedLockCount--;
        } else {
            m_exclusiveLockCount--;
        }
    }
    return ret;
}

bool FileLock::platformUnLock(bool unlockToSharedLock) {
#    ifdef MMKV_ANDROID
    if (m_isAshmem) {
        return ashmemUnLock(unlockToSharedLock);
    }
#    endif
    int cmd = unlockToSharedLock ? LOCK_SH : LOCK_UN;
    auto ret = flock(m_fd, cmd);
    if (ret != 0) {
        MMKVError("fail to unlock fd=%d, ret=%d, error:%s", m_fd, ret, strerror(errno));
        return false;
    } else {
        return true;
    }
}

meta - 元信息

除了保存键值对的数据文件(二进制的磁盘文件,mmap 映射进内存)和用以索引的词典(存在于进程内存空间内的数据结构 std::unordered_map),还有一些信息需要存储和进程间共享,这些信息以 MMKVMetaInfo 的内存结构排列如下

MMKVMetaInfo 是一个磁盘文件,与键值对数据文件处于同一目录且同名,但是以 .crc 结尾;元信息文件被 mmap 进内存后进行读写,下个章节 会讲到 mmap 本身支持多进程,所以每个进程对元信息文件的修改都会被其他进程感知到

m_crcDigest 是一个很重要的字段,它代表所有键值对数据的循环校验码,每当有修改操作发生时就会更新 size 和 crc 并将这两个字段的更新写入 mmap 映射区域,这样其他进程就知道数据发生了改变(进程间通讯的一个例子)

如果修改删除操作是在原来的位置操作,那么每次操作后都需要全量计算 crc,使得修改操作效率低下,好在章节 Update - 修改的情况 说过实际上所有的修改删除操作都是以 append 一个相同 key 的键值对来实现的,这样计算 crc 时就只需做增量计算而不用全量计算

多个进程同时读写一个 mmap 映射区域,需要用锁进行同步,这里用到了 进程锁 里介绍的基于文件锁(meta file fd)的进程锁实现,下一章节 有更详细地介绍这个锁的使用

struct MMKVMetaInfo {
    uint32_t m_crcDigest = 0;    // 所有键值对的循环校验码,如果不一致说明有修改操作
    uint32_t m_version = MMKVVersionSequence;
    uint32_t m_sequence = 0;
    uint8_t m_vector[AES_KEY_LEN] = {};
    uint32_t m_actualSize = 0;

    // confirmed info: it's been synced to file
    struct {
        uint32_t lastActualSize = 0;
        uint32_t lastCRCDigest = 0;
        uint32_t _reserved[16] = {};
    } m_lastConfirmedMetaInfo;

    // meta file 被 mmap 进物理内存以供多进程读写
    // 下面是读写 mmap 映射区域的方法
    void write(void *ptr) const {
        MMKV_ASSERT(ptr);
        memcpy(ptr, this, sizeof(MMKVMetaInfo));
    }

    void writeCRCAndActualSizeOnly(void *ptr) const {
        MMKV_ASSERT(ptr);
        auto other = (MMKVMetaInfo *) ptr;
        other->m_crcDigest = m_crcDigest;
        other->m_actualSize = m_actualSize;
    }

    void read(const void *ptr) {
        MMKV_ASSERT(ptr);
        memcpy(this, ptr, sizeof(MMKVMetaInfo));
    }
};

// 每当有修改操作时(CURD 都是新增键值对),就会更新循环校验码
KVHolderRet_t
MMKV::doAppendDataWithKey(const MMBuffer &data, const MMBuffer &keyData, bool isDataHolder, uint32_t originKeyLength) {
    // ...
    SCOPED_LOCK(m_exclusiveProcessLock);    // 进程间的排它锁/写锁
    // ...
    auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;
    m_actualSize += size;
    updateCRCDigest(ptr, size);    // [ptr, ptr + size] 是新增键值对所在的内存地址区间
    return make_pair(true, KeyValueHolder(originKeyLength, valueLength, offset));
}

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);    // 利用 ZLIB 计算 CRC
    writeActualSize(m_actualSize, m_crcDigest, nullptr, KeepSequence);      // 更新 size 和 crc(m_actualSize 是所有键值对的总大小)
}

bool MMKV::writeActualSize(size_t size, uint32_t crcDigest, const void *iv, bool increaseSequence) {
    // backward compatibility
    oldStyleWriteActualSize(size);

    if (!m_metaFile->isFileValid()) {
        return false;
    }

    bool needsFullWrite = false;
    m_actualSize = size;
    m_metaInfo->m_actualSize = static_cast<uint32_t>(size);  // 更新 size 和 crc
    m_crcDigest = crcDigest;
    m_metaInfo->m_crcDigest = crcDigest;
    if (m_metaInfo->m_version < MMKVVersionSequence) {
        m_metaInfo->m_version = MMKVVersionSequence;
        needsFullWrite = true;
    }
    if (unlikely(increaseSequence)) {
        m_metaInfo->m_sequence++;
        m_metaInfo->m_lastConfirmedMetaInfo.lastActualSize = static_cast<uint32_t>(size);
        m_metaInfo->m_lastConfirmedMetaInfo.lastCRCDigest = crcDigest;
        if (m_metaInfo->m_version < MMKVVersionActualSize) {
            m_metaInfo->m_version = MMKVVersionActualSize;
        }
        needsFullWrite = true;
    }
    if (unlikely(needsFullWrite)) {
        m_metaInfo->write(m_metaFile->getMemory());
    } else {
        // 将 size 和 crc 更新至 meta file mmap 映射区域
        // mmap 本身是支持多进程的,那么其他进程就能感知到 crc 变了
        m_metaInfo->writeCRCAndActualSizeOnly(m_metaFile->getMemory());
    }
    return true;
}

实现多进程

mmap 自身是支持多进程的,void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);flags 有个标志位 MAP_SHARED

  1. 正如 mmap 章节 所说,读写进程虚存里的映射区域 -> 发生缺页,分配并关联一片物理页,从对应的磁盘地址里读取内容至这篇物理页 -> 返回 or 修改物理页的内容

  2. 所有读写相同映射区间的进程,实际上都关联了相同的物理页,也即它们都在读写相同的物理内存地址,故一个进程对映射区域的修改能够被其他进程的读操作感知到

  3. 对于以文件作为 backed 的 mmap,所有修改还会同步至文件

MAP_SHARED
Share this mapping. Updates to the mapping are visible to other processes mapping the same region, and (in the case of file-backed mappings) are carried through to the underlying file. (To precisely control when updates are carried through to the underlying file requires the use of msync(2).)

也就说如果有进程修改了键值对 mmap 映射区域,的确所有进程都能读到最新的数据,但 MMKV 是通过进程内的数据结构 std::unordered_map 作为索引来搜索键值对的,这个 map 并不会在进程间同步,导致搜索出来的总是旧值

于是需要 meta - 元信息,它是与键值对数据文件同目录的、以 .crc 结尾的文件,并 mmap 映射进内存;每当 CURD 操作完成时更新它的 crc 字段,每当 CURD 操作前通过 checkLoadData 检查此文件的 crc 是否与进程内的 crc 一致,不一致说明有修改操作发生,需要重新构建索引

与此同时,在多进程模式下启用读锁 m_sharedProcessLock 和写锁 m_exclusiveProcessLock,所有的 CURD 操作都用 进程锁 上锁保证读写操作的同步(也可以认为是一种进程间通讯的方式)

static public final int SINGLE_PROCESS_MODE = 1 << 0;  // 单进程模式

static public final int MULTI_PROCESS_MODE = 1 << 1;   // 多进程模式

public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, String rootPath)

private native static long getMMKVWithID(String mmapID, int mode, @Nullable String cryptKey, @Nullable String rootPath);

MMKV_JNI jlong getMMKVWithID(JNIEnv *env, jobject, jstring mmapID, jint mode, jstring cryptKey, jstring rootPath)

MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath)

MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath)
    // ...
    , m_metaFile(new MemoryFile(m_crcPath, DEFAULT_MMAP_SIZE, m_file->m_fileType))
    , m_metaInfo(new MMKVMetaInfo())
    , m_crypter(nullptr)
    , m_lock(new ThreadLock())
    , m_fileLock(new FileLock(m_metaFile->getFd(), (mode & MMKV_ASHMEM)))                               // 对 meta file 上文件锁
    , m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))                             // 上面介绍过这是基于文件锁的进程锁
    , m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))                       // 用以在多进程模式下同步 meta file 的读写操作
    , m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0 || (mode & CONTEXT_MODE_MULTI_PROCESS) != 0) {  // 多进程模式下置真
    m_actualSize = 0;
    m_output = nullptr;

    // force use fcntl(), otherwise will conflict with MemoryFile::reloadFromFile()
    m_fileModeLock = new FileLock(m_file->getFd(), true);
    m_sharedProcessModeLock = new InterProcessLock(m_fileModeLock, SharedLockType);
    m_exclusiveProcessModeLock = nullptr;
    m_dic = new MMKVMap();

    m_needLoadFromFile = true;
    m_hasFullWriteback = false;

    m_crcDigest = 0;

    m_sharedProcessLock->m_enable = m_isInterProcess;    // 多进程模式下才启用读锁和写锁(meta file)
    m_exclusiveProcessLock->m_enable = m_isInterProcess;

    // sensitive zone
    {
        SCOPED_LOCK(m_sharedProcessLock);
        loadFromFile();
    }
}

void MMKV::checkLoadData() {     // 所有 CURD 操作前都会检查 crc 是否有改变(CURD 操作后也会更新 crc)
    if (m_needLoadFromFile) {    // 一般情况下为 false,特殊情况比如 clearMemoryCache 时才置真
        SCOPED_LOCK(m_sharedProcessLock);

        m_needLoadFromFile = false;
        loadFromFile();
        return;
    }
    if (!m_isInterProcess) {    // 多进程模式下置真
        return;
    }

    if (!m_metaFile->isFileValid()) {
        return;
    }

    SCOPED_LOCK(m_sharedProcessLock);    // 获得读锁,读取 meta file
    MMKVMetaInfo metaInfo;
    metaInfo.read(m_metaFile->getMemory());

    if (m_metaInfo->m_sequence != metaInfo.m_sequence) {
        MMKVInfo("[%s] oldSeq %u, newSeq %u", m_mmapID.c_str(), m_metaInfo->m_sequence, metaInfo.m_sequence);
        SCOPED_LOCK(m_sharedProcessLock);
        clearMemoryCache();
        loadFromFile();
        notifyContentChanged();

    } else if (m_metaInfo->m_crcDigest != metaInfo.m_crcDigest) {  // CURD 操作导致 crc 不一致
        MMKVDebug("[%s] oldCrc %u, newCrc %u, new actualSize %u", m_mmapID.c_str(), m_metaInfo->m_crcDigest,
                  metaInfo.m_crcDigest, metaInfo.m_actualSize);
        SCOPED_LOCK(m_sharedProcessLock);

        size_t fileSize = m_file->getActualFileSize();  // 如果文件大小不一致说明有扩容(或者 crc 校验不通过导致文件被清空)
        if (m_file->getFileSize() != fileSize) {        // 此时需要重新 mmap 并重新构建索引
            MMKVInfo("file size has changed [%s] from %zu to %zu", m_mmapID.c_str(), m_file->getFileSize(), fileSize);
            clearMemoryCache();
            loadFromFile();

        } else {                                        // 大小不变,因为所有修改操作都是通过 append 相同 key 值的键值对实现的
            partialLoadFromFile();                      // 这里只需要解析新加的键值对即可
        }
        notifyContentChanged();
    }
}

参考

  1. 认真分析 mmap:是什么 为什么 怎么用 - Alan Hu - 博客园

  2. Protocol Buffers - Base 128 Varints