Android MMKV
应该说时Tencent MMKV
github地址:github.com/Tencent/MMK…
看下介绍:
MMKV——基于 mmap 的高性能通用 key-value 组件
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Win32 / POSIX 平台,一并开源。
官方介绍的原理
- 内存准备
通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。 - 数据组织
数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。 - 写入优化
考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。 - 空间增长
使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
使用
很简单,直接看github官网地址就可以。 在application中初始化,activity中使用。
//1.初始化
MMKV rootDir = MMKV.initialize(this)
//2.获取实例
MMKV kv = MMKV.defaultMMKV();
//取值
boolean bValue = kv.decodeBool("bool");
//3.存值
kv.encode("int", Integer.MIN_VALUE);
//4.取值
int iValue = kv.decodeInt("int");
引一张官网的对比图(循环写入随机的int 1k次):
还搞什么SP优化?
要看MMKV的流程,肯定要把源码下载,这里推荐我的小伙伴使用sublime Text来查看源码,非常方便,截个图感受一下
1.初始化
//初始化方法 这里只看关键代码
public static String initialize(Context context) {
//向下调用
initialize(context, root, null, logLevel);
//initialize调用doInitialize
doInitialize(rootDir, cacheDir, loader, logLevel);
//doInitialize调用jniInitialize
//关键代码,看名字就知道是一个jni调用c++代码
jniInitialize(rootDir,cacheDir,logLevel2Int(logLevel));
MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir, jstring cacheDir, jint logLevel) {
if (!rootDir) {
return;
}
const char *kstr = env->GetStringUTFChars(rootDir, nullptr);
if (kstr) {
//调用MMKV.cpp方法
MMKV::initializeMMKV(kstr, (MMKVLogLevel) logLevel);
env->ReleaseStringUTFChars(rootDir, kstr);
g_android_tmpDir = jstring2string(env, cacheDir);
}
}
//MMKV.cpp
void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
g_currentLogLevel = logLevel;
ThreadLock::ThreadOnce(&once_control, initialize);
g_rootDir = rootDir;
mkPath(g_rootDir);
MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());
}
这里我们看,实际上初始化就是做了创建存储的根目录,记录rootDir,没别的事情了。
2.获取实例
//获取实例
MMKV kv = MMKV.defaultMMKV();
/**调用链
*MMKV.class defaultMMKV()
*navtive-bridge.cpp getDefaultMMKV
*MMKV.CPP. mmkvWithID
**/
#ifndef MMKV_ANDROID
MMKV *MMKV::mmkvWithID(const string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath) {
if (mmapID.empty()) {
return nullptr;
}
SCOPED_LOCK(g_instanceLock);
//参数1 mmapID就是keyname 参数2 存储地址
//1.经过md5,生成了一个mmapkey
auto mmapKey = mmapedKVKey(mmapID, rootPath);
//2.通过mmapkey查找是否有对象,有就返回
auto itr = g_instanceDic->find(mmapKey);
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second;
return kv;
}
if (rootPath) {
MMKVPath_t specialPath = (*rootPath) + MMKV_PATH_SLASH + SPECIAL_CHARACTER_DIRECTORY_NAME;
if (!isFileExist(specialPath)) {
mkPath(specialPath);
}
MMKVInfo("prepare to load %s (id %s) from rootPath %s", mmapID.c_str(), mmapKey.c_str(), rootPath->c_str());
}
//3.没有找到就创建一个新的MMKV对象加入到map
auto kv = new MMKV(mmapID, mode, cryptKey, rootPath);
kv->m_mmapKey = mmapKey;
(*g_instanceDic)[mmapKey] = kv;
re
- 1.mmapedKVKey方法通过两个参数mmapID、rootPath,经过med生成mmapkey
- 2.通过mmapkey在g_instanceDic中进行查找是否有相应对象
- 3.没有找到对象,创建一个新的MMKV添加到map中
3.取值
以前面的代码为例
//通过decodeBool取对应的值,调用到了jni的方法
MMKV_JNI jboolean decodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean defaultValue) {
//将okey转换成mmkv对象
MMKV *kv = reinterpret_cast<MMKV *>(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->getBool(key, defaultValue);
}
return defaultValue;
}
bool MMKV::getBool(MMKVKey_t key, bool defaultValue, bool *hasValue) {
if (isKeyEmpty(key)) {
if (hasValue != nullptr) {
*hasValue = false;
}
return defaultValue;
}
SCOPED_LOCK(m_lock);
SCOPED_LOCK(m_sharedProcessLock);
//通过key个人理解时拿到了具柄之类的数据
auto data = getDataForKey(key);
if (data.length() > 0) {
try {
//通过具柄获取到真正的数据,通过input.readBool返回
CodedInputData input(data.getPtr(), data.length());
if (hasValue != nullptr) {
*hasValue = true;
}
return input.readBool();
} catch (std::exception &exception) {
MMKVError("%s", exception.what());
}
}
if (hasValue != nullptr) {
*hasValue = false;
}
return defaultValue;
}
- 1.取值方法通过jni调用到了c++层
- 2.通过mmkv:getBool方法寻找对应的值
- 3.通过getDataForkey拿到数据具柄
- 4.通过CodeInputData方法传入具柄和长度获取到存值
- 5.通过input.readBool将值返回回来
4.存值
//依旧以前面的代码为例
//存值
kv.encode("int", Integer.MIN_VALUE);
//这里有很多重载函数 根据你传入的类型调用到具体的方法
//找一个string方法的
public boolean encode(String key, @Nullable String value) {
return encodeString(nativeHandle, key, value);
}
//调入natvie-bridge.cpp中
MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {
//此处的handler就是通过MMKVWithID获取的一个long类型 对象
//相当于一个mmkv地址,将mmkv地址转换成对象
MMKV *kv = reinterpret_cast<MMKV *>(handle);
if (kv && oKey) {
//将jstring转换成string
string key = jstring2string(env, oKey);
if (oValue) {
string value = jstring2string(env, oValue);
//将值通过key set进去
return (jboolean) kv->set(value, key);
} else {
kv->removeValueForKey(key);
return (jboolean) true;
}
}
return (jboolean) false;
}
bool MMKV::set(bool value, MMKVKey_t key) {
//如果key是空的返回 false
if (isKeyEmpty(key)) {
return false;
}
size_t size = pbBoolSize();
MMBuffer data(size);
//通过具柄拿到数据
CodedOutputData output(data.getPtr(), size);
//将数据写入
output.writeBool(value);
//调用到MMKV_IO.cpp中进行io读写,下面具体说
return setDataForKey(move(data), key);
}
存值的方法和取值类似
- 1.通过方法调用到jni方法中,调用到jni层到C++ encodeString
- 2.通过具柄转换成的mmkv对象
- 3.通过CodeOutputData传入数据具柄找到对应数据
- 4.写入数据,进行io操作
5.mmap内存映射
//构造mmvk对象
auto kv = new MMKV(mmapID, mode, cryptKey, rootPath);
MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath){
// 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;
# ifndef MMKV_DISABLE_CRYPT
//加密算法 加密key,new 一个加密对象AESCrypt
if (cryptKey && cryptKey->length() > 0) {
m_dicCrypt = new MMKVMapCrypt();
m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
} else
# endif
{
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();
}
}
//理论上应该在loadFromFile()中进行mmap。但是并没有找到相关代码
//埋个坑 等后续有时间阅读下官方文档具体看下