MMKV

MMKV简介

在微信客户端的日常运营中,时不时的会爆发出特殊文字引起系统的crash,解决方案是在关键代码前后进行计数器的加减,通过检查计数器的异常,来发现闪退的异常文字,这些计数器还要永久的存储下来——因为闪退随时可能发生。这就需要一个性能非常高的通用key-value存储组件,而SharedPerferenceNSUserDefaultsSQLite等常见组件,发现都没能满足如此苛刻的性能要求。考虑到这个防crash方案最主要的诉求还是实时写入,而mmap内存映射文件刚好满足这个需求,所以通过它来实现一套key-value组件。

cell是单元数组(Cell Array)将类型不同的相关数据集成到一个单一的变量中,使得大量相关数据的引用和处理变得简单;需要注意的是,单元数组仅仅是承载其他数据类型的容器,大部分的数学运算只是针对其中的具体数据进行的,而非针对单元数组本身。

MMKV分别代表的是Memory Mapping Key Value,是基于mmap内存映射的key-value组件,底层序列化/反序列化使用protobuf实现,性能高,稳定性强。

不管是单线程还是多线程,MMKV的读写能力都远远的甩开了SharedPreferencesSQLiteSQLite+Transacion

MMKV原理

内存准备

通过mmap内存映射文件,提供一段可供随时写入的内存块,APP只管往里面写数据,由操作系统负责将内存回写到文件,不必担心crash导致数据丢失。

Memory Mapping内存映射

Memory Mapping简称mmap是一种将磁盘上文件或文件的一部分映射到应用程序地址空间的机制,从而应用程序可以用访问内存的方式访问磁盘文件。

由此可见,mmap的优势很明显,因为进行了内存映射,操作内存相当于操作文件,无需开启新的线程,相较于I/O对文件的读写操作,只需要从磁盘到用户空间的一次数据拷贝,减少了数据的拷贝次数,提高了文件的操作效率;同时mmap只需要提供一段内存,只需要关注往内存文件中读写操作即可,在操作系统内存不足或进程退出时自动写入文件中。

mmap也有自身的劣势,因为mmap需要提供一段长度的内存块,其映射区的长度默认是一页,即4KB,当存储的文件内容较少时可能会造成空间的浪费。

数据组织

数序列化方面选择protobuf协议,pb在性能和空间占用上都有不错的表现。 考虑到要提供的是通用kv组件,key可以限定是string字符串类型,value则是多种多样(int/bool/double等)。要做到通用的话,考虑将value通过protobuf协议序列化成统一的内存块(buffer),然后就可以将这些KV对象序列化到内存中。

Protocol Buffers编码结构

Protocol Buffers简称protobuf,是Google出品的一种可扩展的序列化数据的编码格式,主要用于通信协议和数据存储;利用varint原理(一种可变的编码方式,值越小的数字,使用的字节越少)压缩数据以后,二进制数据非常紧凑,另外,protobuf采用TLV(TAG-Length-Value)的编码格式,减少了分隔符的使用。

protobuf在更新文件时,虽然也不方便局部更新,但是可以做到增量更新,即不管之前是否有相同的key,一旦有新的数据便添加到文件最后,待最终文件读取时,后面新的数据会覆盖之前老旧的数据。

当添加新的数据时文件大小不够了,需要全量更新,此时需要将Map中数据按照MMKV方式序列化,滤重保存需要的字节数,根据获取的字节数与文件大小进行比较;若保存后的文件大小可以添加新的数据时直接添加到最后面,若保存后的文件大小还是不足以添加新的数据时,此时需要对protobuf*2扩容。

protobuf功能简单,作为二进制存储,可读性较差;同时无法表示复杂的概念,通用性相较于.xml较差,这也是protobuf的不足之处

写入优化

标准protobuf不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力:**将增量kv对象序列话后,直接append到内存末尾;**这样同一个key会有新旧若干份数据,最新的数据在最后;那么只需要在程序启动第一次打开mmkv时,不断用后续读入的value替换之前的值,就可以保证数据是最新有效的。

空间增长

使用append实现增量更新带来了一个新的问题,就是不断append的话,文件大小会增长得不可控。例如同一个key不断更新的话,可能耗尽几百M甚至G的空间,而事实上整个kv文件就这一个key,不到1K的空间就能存的下,这明显是不可取的。所以需要在西能和空间上做个折中:以内存pagesize为单位申请空间,在空间用尽之前都是append模式;当append到文件末尾时,进行文件重整、key排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。

flock文件锁+CRC校验

SharedPreferences因为线程安全不支持在多进程中进行数据更新;而MMKV通过flock文件锁和CRC校验支持多进程的读写操作;

MMKV在进程A中更新了数据,在进程B中获取当前数据时会先通过CRC文件校验看文件是否有过更新,若没有更新直接读取,若已经更新则重新获取文件内容在进行读取;

而为了防止多个进程同时对文件进行写操作,MMKV采用了文件锁flock方式来保证同一时间只有一个进程对文件进行写操作。

使用指南

安装引用
dependencies {
    implementation 'com.tencent:mmkv-static:1.2.8'
    // replace "1.2.8" with any available version
}

推荐使用Maven,从v1.2.8起,MMKV迁移到Maven Central。老版本(<=1.2.7)仍然在JCenter

初始化

MMKV的使用简单,所以变更立马生效,无需调用syncapply。在APP启动时初始化MMKV,设定MMKV的根目录(files/mmkv/),例如在Application中:

override fun onCreate() {
  super.onCreate()
    val rootDir = MMKV.initialize(this)
    println("mmkv root: $rootDir") // mmkv root: /data/user/0/com.cah.kotlintest/files/mmkv
}
使用

MMKV提供一个全局的实例,可以直接使用:

var kv = MMKV.defaultMMKV()

kv?.encode("bool", true)
val bValue = kv?.decodeBool("bool")
println("mmkv values, bValue: $bValue") // mmkv values, bValue: true

kv?.encode("int", Int.MAX_VALUE)
val iValue = kv?.decodeInt("int")
println("mmkv values, iValue: $iValue") // mmkv values, iValue: 2147483647

kv?.encode("string", "Hello from mmkv")
val str = kv?.decodeString("string")
println("mmkv values, str: $str") // mmkv values, str: Hello from mmkv


// 另外
val kv = MMKV.defaultMMKV()
kv?.let {
  it.encode("bool", true)
  val bValue = it.decodeBool("bool")
  println("mmkv values, bValue: $bValue") // mmkv values, bValue: true
}
kv?.let {
  it.encode("int", Int.MAX_VALUE)
  val iValue = it.decodeInt("int")
  println("mmkv values, iValue: $iValue") // mmkv values, iValue: 2147483647
}
kv?.let {
  it.encode("string", "Hello from mmkv")
  val str = it.decodeString("string")
  println("mmkv values, str: $str") // mmkv values, str: Hello from mmkv
}

删除&查询

kv?.let {
    it.removeValueForKey("bool")
    println("mmkv remove values, bool: ${it.decodeBool("bool")}") 
    // mmkv remove values, bool: false
}
kv?.let {
    it.removeValuesForKeys(arrayOf("int", "long"))
    it.allKeys()?.forEach { key ->
        println("mmkv remove values, allKeys: $key, ")
    }
    // mmkv remove values, allKeys: string,
    println("mmkv remove values, hasBool: ${it.containsKey("bool")}") 
    // mmkv remove values, hasBool: false
}

如果不同业务需要区分存储,也可以单独创建自己的实例:

val kv = MMKV.mmkvWithID("MyID")
kv?.encode("bool", true)

如果业务需要多进程访问,那么再初始化的时候加上标志位MMKV.MULTI_PROCESS_MODE

val kv = MMKV.mmkvWithID("MyID", MMKV.MULTI_PROCESS_MODE)
kv?.encode("bool", true)

支持的数据类型

  • 支持以下Java语言基础类型:booleanintlongfloatdoublebyte[]
  • 支持以下Java类和容器:StringSet<String>;任何实现了Parcelable的类型

SharedPreferences迁移

MMKV提供了importFromSharedPreferences()函数,可以比较方便地迁移数据过来。

MMKV还额外实现了一遍SharedPreferencesSharedPreferences.Editor这两个interface,在迁移的时候只需要两三行代码,其他CRUD操作代码都不用改。

val preferences = MMKV.mmkvWithID("myData")
// 迁移旧数据
val oldData = getSharedPreferences("myData", MODE_PRIVATE)
preferences?.importFromSharedPreferences(oldData)
oldData.edit().clear().apply()

// 和以前的用法一样
val editor = preferences?.edit()
editor?.let {
  it.putBoolean("bool", true)
  it.putInt("int", Int.MIN_VALUE)
  it.putLong("long", Long.MAX_VALUE)
  it.putFloat("float", -3.14f)
  it.putString("string", "hello, imported")
  val set = HashSet<String>()
  set.add("W")
  set.add("e")
  set.add("C")
  set.add("h")
  it.putStringSet("string-set", set)
  // 无需调用 commit/apply
}

println("mmkv new value, name: ${preferences?.getString("name", "")}") 
// mmkv new value, name: Eileen
println("mmkv new value, string ${preferences?.getString("string", "")}")
// mmkv new value, string hello, imported

MMKV VS SharedPreferences

  • 数据格式及更新范围优化SharedPreferences采用.xml数据存储,每次读写操作都会全量更新;MMKV采用protobuf数据存储,更紧密,支持局部更新
  • 文件耗时操作优化MMKV采用mmap内存映射的方式取代I/O操作,减少拷贝次数,提高更新速度
  • 跨进程状态同步SharedPreferences为了线程安全不支持跨进程状态同步;MMKV通过CRC校验和文件锁flock实现跨进程状态更新;
  • 应用便捷性,较好的兼容性MMKV使用方式便捷,与SharedPreferences基本一致,迁移成本低;

参考

https://blog.csdn.net/xingyu19911016/article/details/115265288

https://www.oschina.net/p/mmkv?hmsr=aladdin1e1

https://my.oschina.net/u/4385177/blog/4645412

https://github.com/Tencent/MMKV/blob/master/README_CN.md

https://www.it610.com/article/1282634563008348160.htm

http://www.manongjc.com/detail/8-obourhfdkjnmpfo.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值