锦囊篇|一文摸懂 SharedPreferences 和 MMKV(二)
MMKV源码分析
初始化 / `MMKV.initialize(this);`
在MMKV
的整套流程中,MMKV
的初始化起着承上启下的作用。
因为到这里的话直接通过三方库的导入已经不能满足查看了,所以直接去下载MMKV
的开源库源码查看比较合适。
如果你并不太熟悉JNI
的方法调度,也没关系,我会慢慢的通过方式来教你入门。
**你能够发现是爆红的JNI
方法,那如何定位呢?** 摁两下Shift
的全局搜索,然后直接输入initializeMMKV
,就会得到搜索结果了。
能够发现这里存在两个方法,进去看看就知道像C
写的,那目标群体就已经被你锁定了。
对象实例获取 / `MMKV.defaultMMKV()`
你能看到***
注释位置是代码中一个迷惑性行为,通过数据类型定义能够知道最后得到的数据是一个数据类型为long
的数据,我们可以猜测这个数据的用处对应着最后能够用于寻找到对应的MMKV
,通过深层次调用后可以发现他调用了一个mmkvWithID()
的方法,其中DEFAULTMMAPID
为mmkv.default
。
在这个代码中总共有两个核心部分:
mmapKey的值的计算: 通过
mmapID
和relativePath
两个值进行一定的运算操作,具体关系就是mmapID
和relativePath
的重合关系,具体还是要见于代码实现。MMKV的生成: 这里的解释对应
注释2
和注释3
,就是通过一个Map
的形式来对数据进行存储,如果在g_instanceDic
这个变量中进行数据查询。
MMKV的内部结构
和SharedPreferences
相同最后还是需要经历一场和文件读写的殊死搏斗,那问题就来了,同样是文件读写,为什么MMKV
能够以百倍的速度碾压各类已成熟的产品呢?从我的思路出发可以分为这样的几种情况:
不够健壮的错误数据处理。 这如果你做一个简易版的
FastJson
就能够发现,数据的处理速度基本上能够有非常高的提升。但是这对于相对成熟的产品而言一般不会有这种方案。底层进行数据处理。 这个方案的推行在一定程度上也是对应现在的两者对比有一定的道理,因为能够发现
MMKV
的实现方案基本都是依靠JNI
来调度完成,而C
的处理速度和Java
相比想来我们也是有目共睹的。更优化的文件读取方案。 这就是对当前方案的分析了,因为还没有看到后面的代码,所以这里是一种方案的猜测。因为
SharedPreferences
和MMKV
两者都是我们有目共睹需要对数据进行读写操作的,而数据的最后来源就是本地的文件,一个更易于读写的文件方案势必是一个最关键的突破点。。。。。。接下来由你开始进行更多的思考。
**回归正题:loadFromFile();
**
在刚刚的猜想中,我提及了关于文件读写的问题,因为对MMKV
而言,文件读写这一关肯定是躲不过去的,但是如何更高效就是我们应该去思考的点了。
在代码段中我标注出了注释1
和注释2
,也是我认为至关重要的代码了,分别做了两大操作:
数据的写回方案制作: 这是要一个非常有特色的地方,为什么这么说呢?其实你能够从一个判断的变量名能够看出会对数据的写回方式有一个选择,也就是部分写回和全部写回的策略之选,那这就是第一个原因为什么
MMKV
的综合性能能够强过SharedPreferences
。文件格式的选择: 其实这是解析时候的事情了,这一段的论证来源于 MMKV 原理,
protobuf
作为MMKV
最后的选择方案在性能和空间占用上都有不错的表现。
数据更新 / `kv.encodeXXX("string", XXX);`
这里的代码分析只拿一个作为样例即可
关注几个注释点:
注释1: 这就是之前在上面的时候已经提到过的在
Java
这一层中进行的操作只是一个数据类型为long
的handle
变量进行赋值操作,而这个handle
中在后期可以被解析转化为已经初始化完成的MMKV
对象。注释2: 完成相对应的数据放置操作,那这里就要观察代码的深层调度是一个怎么样的过程了。
但是通过官方的文档中能够知道,关于这个文件格式下的数据是存在问题的,那就是他并不支持增量更新 ,这也就意味着复杂的操作会更加多了,那腾讯的解决方案是什么呢?
标准
protobuf
不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力:将增量kv
对象序列化后,直接append
到内存末尾;这样同一个key
会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开mmkv
时,不断用后读入的value
替换之前的值,就可以保证数据是最新有效的。
一句话讲来就是,新的或更改过的就最后新增后面插入。
**而新旧数据累加势必会造成文件的庞大,那这方面MMKV
给出的解决方案又是怎么样的呢?**
以内存
pagesize
为单位申请空间,在空间用尽之前都是append
模式;当append
到文件末尾时,进行文件重整、key
排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。
同样的换成一句话来进行描述,有上限目标的文件重写。
这一段的代码实现就不贴出了,具体位置就在MMKV_IO
中的ensureMemorySize()
方法,通过已存在数据大小的总量来进行整理,因为很多时候数据量很大是因为大容量的数据的重复添加造成的。
数据获取 / `kv.decodeXXX("string");`
其实基本逻辑和写文件的差不多了,这个时候还是首先要获取一个对应的MMKV
对象,然后完成数据的获取。
转化为CodedInputData
的对象来完成数据的读取,如果数据不存在,那就直接默认值返回。
删除对应的数据 / `kv.removeValueForKey("string")`
在看代码之前做一个思考,在已知的数据基础上,换成你会怎么做这样的操作呢?
我们要关注的点有以下几个:
protobuf
是一个不支持增量更新的文件格式,相对应MMKV
给出的解决方案就是通过尾部增加,出现新旧数据叠加从
问题1
的引申,新旧数据叠加的一个查询和删除问题,因为新旧数据,那么做查询的时候势必要多次的查,如果每次的数据都有1G
,那你的查询每次都要叠加到1G
的程度,而不是查到即可开始删除。
对于以上问题思考清楚了的话,我们就可以给出MMKV
的解决方案了。
将关注点全部放置于注释带*
的代码段上,一个没有赋值的MMBuffer
说明数据为空,然后直接调用appendDataWithKey()
文件写入的方案,说明最后出现在protobuf
的数据样式会是这样的。
其实就是往里面加一个新的空数据作为新的数据。
总结
从源码分析完之后,和SharedPreferences
相比,重新整理后可以总结为以下几点的突破:
**
mmap
的使用: 内存映射的技术的使用,减少了SharedPreferences
** 的拷贝和提交的时间消耗。数据的更新方式: 局部更新的数据,通过尾部追加来进行完成,而不是像
SharedPreferences
一样的直接文件重构。同样要注意这样的方式会造成冗余数据的增加。多进程访问安全的设计: 详细见于MMKV for Android 多进程设计与实现,主要还是以
mmap
作为突破口,来完成对其他进程对当前文件的操作的一个状态感知,主要就是分为三方面:写指针增长、内存重整、内存增长 。
版权声明: 本文为 InfoQ 作者【ClericYi】的原创文章。
原文链接:【http://xie.infoq.cn/article/ada7b02bc372b8fca32627355】。文章转载请联系作者。
评论