开发中,咱们总会需求存储些 KV
数据,虽然看上去简略,但考虑因素也是许多的,完成手段也就各有差异。今日,咱们就来看看 Android
现在有哪些 KV
库能够供咱们运用,以及其有哪些优缺点。
SharedPreferences
这个是 Android
很早就内置的 KV
存储库了, 不过由于其缺点多,现在除了拿来做源码剖析和数说外,就没人会推荐运用了。其被诟病的点首要有以下几个:
- 首次子线程加载,在加载未完结时,主线程读取会堵塞等候加载完结。
- 会整个文件加载到内存,并且是 xml 格局。所以假如存储许多
kv
,首次加载会很耗时,并且占内存,占存储。 -
commit
会堵塞写文件,并且是每次写入整个文件,所以一般用apply
。 -
waitToFinish
会堵塞主线程,或许造成 ANR。
Jetpack DataStore
官方终于不想听开发者对 sp
的吐槽了,然后开发了 DataStore
这个新库,比较于 sp
, 它有以下几个改进:
- 运用协程、Flow 等东西,在
Dispatchers.IO
读写,不会堵塞主线程,当然这也导致必须在协程环境调用。 - 能够运用
proto
来序列化数据,因此存储体积会小许多 - 不需求调用
commit
/apply
等
但其依旧是初始化时读取整个文件,写入时掩盖整个文件,关于许多 kv
的场景依旧不友好。
MMKV
MMKV 是 Tencent
开源的跨平台 kv
存储,其长处是:
- 能够跨平台运用,假如有跨平台需求,这是不二之选
- 文件增量写入,运用 proto 序列化数据,并且运用了
mmap
,所以写入很快。
问:文件增量是一个怎样的逻辑?
答:每次写入一个 kv
, 就直接在文件末尾追加这个 kv
的数据。当然,假如屡次写入同一个 key
,那么就会在文件中写入多份数据,这是以空间换时间的思路。在首次加载时,会从文件开端读到文件结束,关于重复 key
, 后读的就会掩盖前面读的,所以假如有许多重复,这儿就会存在额外耗时,当然,MMKV
会进行数据的重收拾,便是把内存数据重新落盘,这样就去掉了重复 key
。
而 MMKV
依旧是会把一切数据一次性加载到内存,所以依旧不能用于许多 kv
的状况。
LevelDB
LevelDB 是 google
开源的,基于 LSM-Tree(Log Structured Merge Trees)
。
首先咱们来了解下它的作业原理。
其中心结构为:
-
MemTable
: 一个内存kv
表, 其key
是有序的。 -
SSTable
:将其MemTable
落地到磁盘,便是SSTable
了,其key
是有序的,所以能够二分查找。 -
WAL
:write ahead log
, 在写数据时,先写入WAL
, 再写入MemTable
, 首要用于数据恢复,例如假如MemTable
还没落地就杀死了进程,那么重启时能够用WAL
恢复出MemTable
。
其具体作业流程为:
写数据
- 写
WAL
- 写入
MemTable
- 假如
MemTable
多到一定程度,则落地成一个SSTable
。
读数据
- 先看
MemTable
里有没有,假如有,则代表是刚写过的,直接回来,假如没有,则进行下一步。 - 查找最新落地的
SSTable
,假如有,则回来,假如没有,则寻觅下一个SSTable
,知道最后一个SSTable
。
当然,假如落地了十分多的 SSTable
, 假如最后一个 SSTable
才存在咱们的数据,那查找就会十分耗时,所以这儿就有许多优化。诸如:SSTable
合并,布隆过滤器、Cache等。
综上,LevelDB
的写入很快,读取反而很复杂的库。很适用于写多于读的场景。当然它读取还是很可观的。并且它不会把一切数据读入内存,所以终于是一个能够用于许多 kv
的场景了。
可是,它是一个纯 c++
库,并不是面向 Android
而生,当然有人把它接入了 Android
,例如:hf/leveldb-android,使得 Android
也能运用它,惋惜的是,这个库并没有继续更新了。
并且 LevelDB
会发生 Lock
文件以防止其它进程运用,但假如我强杀进程,这个文件还在,所以 Open
或许会失败,需求再主动删文件。。。
EmoKV
最后,就要来吹一波自己新造的这个轮子了,具体用法能够前往官网检查 造这个轮子的首要原因是,LevelDB
的的确确不更新了,并且关于 Android
, 或许依旧是读大于写多一点。所以我思考了下,搞了这个库,趁便实践了下 c++
(写起来真特么苦楚~)。
EmoKV
首要有以下几个结构:
-
Key
文件: 存储key
值的文件,选用mmap
+ 追加写的办法 -
Value
文件: 存储value
值的文件,选用mmap
+ 追加写的办法 -
Index
文件: 一个文件HashMap
的完成, 一切kv
都记录在这儿,每个kv
固定20
字节(存储了flag
、key
在Key
文件的方位以及value
在Value
文件的方位)。
其操作流程为:
-
写数据:经过
key
定位Index
方位,假如key
不存在,则写Key
文件,然后写Value
文件,这两者都是追加写。写完更新Index
文件信息。 -
读数据:经过
key
定位Index
方位,然后经过Index
信息读取Value
文件。
由于直接凭借了 mmap
, 所以其读写速度都是很快的,并且确保了数据刷入磁盘(当然这对错强制宕机的状况)。并且 mmap
也是在拜访时才真的读取磁盘页,实际内存占用不大,并且能够由系统办理刷回。
当然,由于运用 mmap
,所以也有潜在的问题:
- 数据太多导致虚拟内存竭尽。当然这还是有难度的,即使是10w个
kv
,Index
用量大约为:10w * 20 / 0.75
大约2.6M
的空间(0.75 为加载因子),Value
以均匀500
字节的长度,大约48M
的长度。关于一般使用,这个是满足的了。当然假如在非紧缩,不使用CRC32
的状况,Index
能够直接存储8字节以下的value
,空间就更可控了。 - 假如
key
算出来hash
抵触很高,由于选用线性勘探法,也会导致读写功能变差。Index
文件还存在扩容的重量级操作。 - 由于
Key
和Value
都选用追加写,在屡次重复kv
写入后,Value
就会存在许多过期数据,所以EmoKV
依然有重收拾的过程。
当然,由于是个人开发的这个库,就还能聊更多细节的点:
-
EmoKV
选用写加锁,读没有加锁,而是选用乐观锁StampedLock
相似的办法。每次写时增加版本号,读取时,先读取当时版本号,假如读取完之后,发现版本号变了,假如改变版本大于1或许且最近一次修改为当时kv
, 则重新读。由于一般读比写快,并且读写抵触概率低,这样更合理 - 由于写分了多个过程,所以要考虑写入过程中进程退出的状况:我这儿选用的办法是写入前先备份索引数据,并先写标记位,假如初始化时发现有标记时,就将备份数据还原回去,上一份数据失败。当然,运用者还能够开启
crc32
校验数据。 - 关于数据紧缩和
crc32
校验,一般的想法都是在c++
上完成,不过我嫌写起来困难,就在kotlin
层调用内置库办法完成了。 - 运用
raii
办理内存,所以内存泄漏的或许性很低。
读写功能比照
最后,我简略比照了 LevelDB
、MMKV
、EmoKV
的读写速度。(本来计划用 benchmark
的,不过不知为何,我的 win
机一向跑不起来,报 Activity Missing
,各种查找都没处理。只得抛弃,用单元测试时间替代。)
个人测试的是 1w
数据的读写。
写入数据
单线程读取
多线程读取
由于并不是真的 benchmark
,所以数据具有偶然性,但大体数量级没差。假如你有爱好,也能够依据不同完成计划,扼要剖析下为何数据是这个姿态?
最后,这个是个人第一次 c++
实战,或许有诸多不正确不完美的地方,欢迎我们来做 Code Review
。