MMKV.initialize(this);
在MMKV
的整套流程中,MMKV
的初始化起着承上启下的做用。java
public static String initialize(Context context) {
// 获取根路径
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
return initialize(root, (MMKV.LibLoader)null, logLevel); // 进行加载 1 -->
}
// 1 -->
public static String initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel) {
// 加载必要的so文件
// 。。。。。
// 经过JNI来对底层c的实现进行初始化调度
jniInitialize(MMKV.rootDir, logLevel2Int(logLevel));
return rootDir;
}
复制代码
由于到这里的话直接经过三方库的导入已经不能知足查看了,因此直接去下载MMKV
的开源库源码查看比较合适。android
若是你并不太熟悉JNI
的方法调度,也不要紧,我会慢慢的经过方式来教你入门。git
JNI
方法,那如何定位呢? 摁两下
Shift
的全局搜索,而后直接输入
initializeMMKV
,就会获得搜索结果了。
可以发现这里存在两个方法,进去看看就知道像C
写的,那目标群体就已经被你锁定了。github
void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
// ThreadOnce说明初始化过程只会进行一次
ThreadLock::ThreadOnce(&once_control, initialize);
g_rootDir = rootDir;
// 对目标路径进行设置,层级递推若是不存在就建立。
mkPath(g_rootDir);
}
复制代码
MMKV.defaultMMKV()
public static MMKV defaultMMKV() {
// 能够设置为多进程模式
// 重点所在
long handle = getDefaultMMKV(SINGLE_PROCESS_MODE, null); // *** -->
return new MMKV(handle); // 1 -->
}
// 1 -->
// 就是一个为long类型的handle变量设置
private MMKV(long handle) {
nativeHandle = handle;
}
复制代码
你能看到***
注释位置是代码中一个迷惑性行为,经过数据类型定义可以知道最后获得的数据是一个数据类型为long
的数据,咱们能够猜想这个数据的用处对应着最后可以用于寻找到对应的MMKV
,经过深层次调用后能够发现他调用了一个mmkvWithID()
的方法,其中DEFAULT_MMAP_ID
为mmkv.default
。安全
MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {
// 。。。。。。
auto mmapKey = mmapedKVKey(mmapID, relativePath); // 1 -->
auto itr = g_instanceDic->find(mmapKey);
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second; // 2 -->
return kv;
}
if (relativePath) {
if (!isFileExist(*relativePath)) {
if (!mkPath(*relativePath)) {
return nullptr;
}
}
}
auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath); // 3 -->
(*g_instanceDic)[mmapKey] = kv;
return kv;
}
复制代码
在这个代码中总共有两个核心部分:app
mmapID
和relativePath
两个值进行必定的运算操做,具体关系就是mmapID
和relativePath
的重合关系,具体仍是要见于代码实现。注释2
和注释3
,就是经过一个Map
的形式来对数据进行存储,若是在g_instanceDic
这个变量中进行数据查询。MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath)
: m_mmapID(mmapedKVKey(mmapID, relativePath)) // historically Android mistakenly use mmapKey as mmapID
, m_path(mappedKVPathWithID(m_mmapID, mode, relativePath)) // 1 -->
, m_crcPath(crcPathWithID(m_mmapID, mode, relativePath))
, m_dic(nullptr)
, m_dicCrypt(nullptr)
, m_file(new MemoryFile(m_path, size, (mode & MMKV_ASHMEM) ? MMFILE_TYPE_ASHMEM : MMFILE_TYPE_FILE))
, 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;
if (cryptKey && cryptKey->length() > 0) {
m_dicCrypt = new MMKVMapCrypt();
m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
} else {
m_dic = new MMKVMap();
}
// 。。。。。。一些赋值操做
// sensitive zone
{
SCOPED_LOCK(m_sharedProcessLock);
loadFromFile();
}
}
复制代码
和SharedPreferences
相同最后仍是须要经历一场和文件读写的殊死搏斗,那问题就来了,一样是文件读写,为何MMKV
可以以百倍的速度碾压各种已成熟的产品呢?从个人思路出发能够分为这样的几种状况:源码分析
FastJson
就可以发现,数据的处理速度基本上可以有很是高的提高。可是这对于相对成熟的产品而言通常不会有这种方案。MMKV
的实现方案基本都是依靠JNI
来调度完成,而C
的处理速度和Java
相比想来咱们也是有目共睹的。SharedPreferences
和MMKV
二者都是咱们有目共睹须要对数据进行读写操做的,而数据的最后来源就是本地的文件,一个更易于读写的文件方案势必是一个最关键的突破点。回归正题:loadFromFile();
post
在刚刚的猜测中,我说起了关于文件读写的问题,由于对MMKV
而言,文件读写这一关确定是躲不过去的,可是如何更高效就是咱们应该去思考的点了。性能
void MMKV::loadFromFile() {
// 文件不合法就从新加载
if (!m_file->isFileValid()) {
m_file->reloadFromFile();
}
// 文件依旧不合法就报错
if (!m_file->isFileValid()) {
MMKVError("file [%s] not valid", m_path.c_str());
} else {
// 进入这一步至少说明文件是合法的,可是须要进行数据的校验
// error checking
bool loadFromFile = false, needFullWriteback = false;
checkDataValid(loadFromFile, needFullWriteback);
auto ptr = (uint8_t *) m_file->getMemory();
// loading
if (loadFromFile && m_actualSize > 0) {
MMBuffer inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
if (m_crypter) {
clearDictionary(m_dicCrypt);
} else {
clearDictionary(m_dic);
}
// 1 -->
if (needFullWriteback) {
if (m_crypter) {
MiniPBCoder::greedyDecodeMap(*m_dicCrypt, inputBuffer, m_crypter); // 2 -->
} else {
MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer); // 2 -->
}
} else {
// 1 -->
if (m_crypter) {
MiniPBCoder::decodeMap(*m_dicCrypt, inputBuffer, m_crypter); // 2 -->
} else {
MiniPBCoder::decodeMap(*m_dic, inputBuffer); // 2 -->
}
}
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
m_output->seek(m_actualSize); // 计算出数据量的实际大小
if (needFullWriteback) {
fullWriteback();
}
} else {
// 若是数据是不合法或者空的就直接丢弃。
// 。。。。。。
}
}
m_needLoadFromFile = false;
}
复制代码
在代码段中我标注出了注释1
和注释2
,也是我认为相当重要的代码了,分别作了两大操做:优化
MMKV
的综合性能可以强过SharedPreferences
。protobuf
做为MMKV
最后的选择方案在性能和空间占用上都有不错的表现。kv.encodeXXX("string", XXX);
这里的代码分析只拿一个做为样例便可
MMKV_JNI jboolean encodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean value) {
MMKV *kv = reinterpret_cast<MMKV *>(handle); // 1-->
if (kv && oKey) {
string key = jstring2string(env, oKey); // 将key再进行特殊的加工处理
return (jboolean) kv->set((bool) value, key); // 2 -->
}
return (jboolean) false;
}
复制代码
关注几个注释点:
Java
这一层中进行的操做只是一个数据类型为long
的handle
变量进行赋值操做,而这个handle
中在后期能够被解析转化为已经初始化完成的MMKV
对象。bool MMKV::set(bool value, MMKVKey_t key) {
// 1. 进行数据的测量,并建立相同大小的区间
size_t size = pbBoolSize();
MMBuffer data(size);
// 2. 转化为CodedOutputData对象用于写入
CodedOutputData output(data.getPtr(), size);
output.writeBool(value); // 3 -->
// 从名字就能知道这实际上是一个正式的数据替换操做
// 追溯后能够发现会出现一个文件的写入。
return setDataForKey(move(data), key);
}
// 3-->
void CodedOutputData::writeBool(bool value) {
// 用0和1来表示最后的数值
this->writeRawByte(static_cast<uint8_t>(value ? 1 : 0));
}
复制代码
可是经过官方的文档中可以知道,关于这个文件格式下的数据是存在问题的,那就是他并不支持增量更新 ,这也就意味着复杂的操做会更加多了,那腾讯的解决方案是什么呢?
标准
protobuf
不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,咱们须要有增量更新的能力:将增量kv
对象序列化后,直接append
到内存末尾;这样同一个key
会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开mmkv
时,不断用后读入的value
替换以前的值,就能够保证数据是最新有效的。
一句话讲来就是,新的或更改过的就最后新增后面插入。
而新旧数据累加势必会形成文件的庞大,那这方面MMKV
给出的解决方案又是怎么样的呢?
之内存
pagesize
为单位申请空间,在空间用尽以前都是append
模式;当append
到文件末尾时,进行文件重整、key
排重,尝试序列化保存排重结果;排重后空间仍是不够用的话,将文件扩大一倍,直到空间足够。
一样的换成一句话来进行描述,有上限目标的文件重写。
这一段的代码实现就不贴出了,具体位置就在MMKV_IO
中的ensureMemorySize()
方法,经过已存在数据大小的总量来进行整理,由于不少时候数据量很大是由于大容量的数据的重复添加形成的。
kv.decodeXXX("string");
MMKV_JNI jboolean decodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean defaultValue) {
MMKV *kv = reinterpret_cast<MMKV *>(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->getBool(key, defaultValue);
}
return defaultValue;
}
复制代码
其实基本逻辑和写文件的差很少了,这个时候仍是首先要获取一个对应的MMKV
对象,而后完成数据的获取。
bool MMKV::getBool(MMKVKey_t key, bool defaultValue) {
auto data = getDataForKey(key);
if (data.length() > 0) {
CodedInputData input(data.getPtr(), data.length());
return input.readBool();
}
return defaultValue;
}
复制代码
转化为CodedInputData
的对象来完成数据的读取,若是数据不存在,那就直接默认值返回。
kv.removeValueForKey("string")
在看代码以前作一个思考,在已知的数据基础上,换成你会怎么作这样的操做呢?
咱们要关注的点有如下几个:
protobuf
是一个不支持增量更新的文件格式,相对应MMKV
给出的解决方案就是经过尾部增长,出现新旧数据叠加问题1
的引伸,新旧数据叠加的一个查询和删除问题,由于新旧数据,那么作查询的时候势必要屡次的查,若是每次的数据都有1G
,那你的查询每次都要叠加到1G
的程度,而不是查到便可开始删除。对于以上问题思考清楚了的话,咱们就能够给出MMKV
的解决方案了。
auto itr = m_dic->find(key);
if (itr != m_dic->end()) {
m_hasFullWriteback = false;
static MMBuffer nan; // ******
auto ret = appendDataWithKey(nan, itr->second); // ******
if (ret.first) {
#ifdef MMKV_APPLE
[itr->first release];
#endif
m_dic->erase(itr);
}
return ret.first;
}
复制代码
将关注点所有放置于注释带*
的代码段上,一个没有赋值的MMBuffer
说明数据为空,而后直接调用appendDataWithKey()
文件写入的方案,说明最后出如今protobuf
的数据样式会是这样的。
message empty{
}
复制代码
其实就是往里面加一个新的空数据做为新的数据。
从源码分析完以后,和SharedPreferences
相比,从新整理后能够总结为如下几点的突破:
mmap
的使用: 内存映射的技术的使用,减小了 SharedPreferences
的拷贝和提交的时间消耗。SharedPreferences
同样的直接文件重构。一样要注意这样的方式会形成冗余数据的增长。mmap
做为突破口,来完成对其余进程对当前文件的操做的一个状态感知,主要就是分为三方面:写指针增加、内存重整、内存增加 。