基于 mmap 的高性能通用 key-value 组件, 底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。 https://github.com/Tencent/MMKV
官方对比:
IOS 循环写入随机的int 1w 次 |
Android 循环写入随机的int 1k 次 |
 |
 |
运行过程
- 通过 mmap 系统调用,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失
- 在内存中维护一份 k,v 字典数据
- 写入操作:将 value protobuf 序列化更新到字典,然后将字典再 protobuf 序列化追加到文件中(每个 Key 的更新都是直接追加到文件末尾,不是覆盖,只有当文件大小不足时才会进行 key 去重操作,所以文件大小会比通过常规方式(SharedPreferences)储存的 xml 文件大)
- 读取操作:将文件中的数据反序列化到内存的字典中,之后的每次 get 操作直接从内存中获取,获取到 value 之后再反序列化
原理
获取 MMKV 实例
1
| private native static long getMMKVWithID(String mmapID, int mode, String cryptKey);
|
对应的 Native 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| extern "C" JNIEXPORT JNICALL jlong Java_com_tencent_mmkv_MMKV_getMMKVWithID( JNIEnv *env, jobject obj, jstring mmapID, jint mode, jstring cryptKey) { MMKV *kv = nullptr; if (!mmapID) { return (jlong) kv; } string str = jstring2string(env, mmapID);
if (cryptKey != nullptr) { string crypt = jstring2string(env, cryptKey); if (crypt.length() > 0) { kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt); } } if (!kv) { kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr); }
return (jlong) kv; }
|
进一步调用 MMKV 类的静态方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| static MMKV *mmkvWithID(const std::string &mmapID, int size = DEFAULT_MMAP_SIZE, MMKVMode mode = MMKV_SINGLE_PROCESS, std::string *cryptKey = nullptr);
MMKV *MMKV::mmkvWithID(const std::string &mmapID, int size, MMKVMode mode, string *cryptKey) {
if (mmapID.empty()) { return nullptr; } SCOPEDLOCK(g_instanceLock);
auto itr = g_instanceDic->find(mmapID); if (itr != g_instanceDic->end()) { MMKV *kv = itr->second; return kv; } auto kv = new MMKV(mmapID, size, mode, cryptKey); (*g_instanceDic)[mmapID] = kv; return kv; }
|
题外话,MMKV 中的锁
MMKV 中封装了2个与锁相关的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class ThreadLock { private: pthread_mutex_t m_lock;
public: ThreadLock(); ~ThreadLock();
void lock(); bool try_lock(); void unlock(); };
ThreadLock::ThreadLock() { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&m_lock, &attr); pthread_mutexattr_destroy(&attr); }
ThreadLock::~ThreadLock() { pthread_mutex_destroy(&m_lock); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| template <typename T> class ScopedLock { T *m_lock;
ScopedLock(const ScopedLock<T> &other) = delete;
ScopedLock &operator=(const ScopedLock<T> &other) = delete;
public: ScopedLock(T *oLock) : m_lock(oLock) { assert(m_lock); lock(); }
~ScopedLock() { unlock(); m_lock = nullptr; }
void lock() { if (m_lock) { m_lock->lock(); } }
bool try_lock() { if (m_lock) { return m_lock->try_lock(); } return false; }
void unlock() { if (m_lock) { m_lock->unlock(); } } };
#define SCOPEDLOCK(lock) _SCOPEDLOCK(lock, __COUNTER__) #define _SCOPEDLOCK(lock, counter) __SCOPEDLOCK(lock, counter) #define __SCOPEDLOCK(lock, counter) ScopedLock<decltype(lock)> __scopedLock##counter(&lock)
|
TODO ⬇️
内存准备
通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
- 默认文件大小:系统内存分页大小,大小一般是 4096 byte, 4KB
- 文件存不够时,先对 key 去重,之后空间还是不够就增大文件,以内存分页大小的整数倍增加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| void MMKV::loadFromFile() { m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU); if (m_fd < 0) { MMKVError("fail to open:%s, %s", m_path.c_str(), strerror(errno)); } else { m_size = 0; struct stat st = {0}; if (fstat(m_fd, &st) != -1) { m_size = static_cast<size_t>(st.st_size); } if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) { size_t oldSize = m_size; m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE; if (ftruncate(m_fd, m_size) != 0) { m_size = static_cast<size_t>(st.st_size); } zeroFillFile(m_fd, oldSize, m_size - oldSize); } m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); if (m_ptr == MAP_FAILED) { MMKVError("fail to mmap [%s], %s", m_mmapID.c_str(), strerror(errno)); } else { memcpy(&m_actualSize, m_ptr, Fixed32Size); bool loaded = false; if (m_actualSize > 0) { if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) { if (checkFileCRCValid()) { m_dic = MiniPBCoder::decodeMap(inputBuffer); m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize, m_size - Fixed32Size - m_actualSize); loaded = true; } } } } } m_needLoadFromFile = false; }
|
数据组织
数据序列化方面选用 protobuf 协议
写入优化
考虑到主要使用场景是频繁地进行写入更新,将 kv 对象序列化后,append 到内存末尾。
比如写入一个字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| bool MMKV::setStringForKey(const std::string &value, const std::string &key) { if (key.empty()) { return false; } auto data = MiniPBCoder::encodeDataWithObject(value); return setDataForKey(std::move(data), key); }
bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) { if (data.length() == 0 || key.empty()) { return false; } checkLoadData();
auto itr = m_dic.find(key); if (itr == m_dic.end()) { itr = m_dic.emplace(key, std::move(data)).first; } else { itr->second = std::move(data); } return appendDataWithKey(itr->second, key); }
|
空间增长
使用 append 实现增量更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| bool MMKV::appendDataWithKey(const MMBuffer &data, const std::string &key) {
bool hasEnoughSize = ensureMemorySize(size);
if (!hasEnoughSize || !isFileValid()) { return false; } if (m_actualSize == 0) { auto allData = MiniPBCoder::encodeDataWithObject(m_dic); if (allData.length() > 0) { writeAcutalSize(allData.length()); m_output->writeRawData(allData); return true; } return false; } else { writeAcutalSize(m_actualSize + size); m_output->writeString(key); m_output->writeData(data); auto ptr = (uint8_t *) m_ptr + Fixed32Size + m_actualSize - size; if (m_crypter) { m_crypter->encrypt(ptr, ptr, size); } updateCRCDigest(ptr, size, KeepSequence);
return true; } }
|
总结
- 适应于频繁写入读取的地方
- 文件大小会比常规方式大一些
- 内部的一些实现比较高效
- 函数传参采用右值引用,外部通过
move
调用将函数中局部变量的内容直接通过移动内存指向形参中,避免直接传参的内存拷贝