MMKV - 基于 mmap 的高性能通用 key-value 组件
MMKV 是基于 mmap 内存映射的移动端通用 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。
一、MMKV由来
二、MMKV原理
内存准备
通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
数据组织
数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
写入优化
考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
空间增长
使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
三、MMKV for Android 特有功能
我们不是简简单单地照搬 iOS 的实现,在迁移到 Android 的过程中,深入分析了 Android 平台现有 kv 组件的痛点,在原有功能基础上,开发了 Android 特有的功能。
多进程访问
通过与 Android 开发同学的沟通,了解到系统自带的 SharedPreferences 对多进程的支持不好。现有基于 ContentProvider 封装的实现,虽然多进程是支持了,但是性能低下,经常导致 ANR。考虑到 mmap 共享内存本质上的多进程共享的,我们在这个基础上,深入挖掘了 Android 系统的能力,提供了可能是业界最高效的多进程数据共享组件。
匿名内存
在多进程共享的基础上,考虑到某些敏感数据(例如密码)需要进程间共享,但是不方便落地存储到文件上,直接用 mmap 不合适。我们了解到 Android 系统提供了 Ashmem 匿名共享内存的能力,发现它在进程退出后就会消失,不会落地到文件上,非常适合这个场景。我们很愉快地提供了 Ashmem MMKV 的功能。
数据加密
不像 iOS 提供了硬件层级的加密机制,在 Android 环境里,数据加密是非常必须的。MMKV 使用了 AES CFB-128 算法来加密/解密。我们选择 CFB 而不是常见的 CBC 算法,主要是因为 MMKV 使用 append-only 实现插入/更新操作,流式加密算法更加合适。事实上这个功能也回馈到了 iOS 版,所以现在两个系统的 MMKV 都有加密功能。
四、MMKV的使用和封装
首先,在build.gradle里面引入库:
implementation 'com.tencent:mmkv:1.0.19'
在Application里面进行初始化:
MMKV.initialize(getApplication());
封装工具类代码:
object SpUtil { var mmkv: MMKV? = null init { mmkv = MMKV.defaultMMKV() } fun encode(key: String, value: Any?) { when (value) { is String -> mmkv?.encode(key, value) is Float -> mmkv?.encode(key, value) is Boolean -> mmkv?.encode(key, value) is Int -> mmkv?.encode(key, value) is Long -> mmkv?.encode(key, value) is Double -> mmkv?.encode(key, value) is ByteArray -> mmkv?.encode(key, value) is Nothing -> return } } fun <T : Parcelable> encode(key: String, t: T?) { if(t ==null){ return } mmkv?.encode(key, t) } fun encode(key: String, sets: Set<String>?) { if(sets ==null){ return } mmkv?.encode(key, sets) } fun decodeInt(key: String): Int? { return mmkv?.decodeInt(key, 0) } fun decodeDouble(key: String): Double? { return mmkv?.decodeDouble(key, 0.00) } fun decodeLong(key: String): Long? { return mmkv?.decodeLong(key, 0L) } fun decodeBoolean(key: String): Boolean? { return mmkv?.decodeBool(key, false) } fun decodeFloat(key: String): Float? { return mmkv?.decodeFloat(key, 0F) } fun decodeByteArray(key: String): ByteArray? { return mmkv?.decodeBytes(key) } fun decodeString(key: String): String? { return mmkv?.decodeString(key, "") } fun <T : Parcelable> decodeParcelable(key: String, tClass: Class<T>): T? { return mmkv?.decodeParcelable(key, tClass) } fun decodeStringSet(key: String): Set<String>? { return mmkv?.decodeStringSet(key, Collections.emptySet()) } fun removeKey(key: String) { mmkv?.removeValueForKey(key) } fun clearAll() { mmkv?.clearAll() } }