MMKV(1)

内存准备

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

数据组织

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

objectivec 复制代码
message KV {
	string key = 1;
	buffer value = 2;
}

-(BOOL)setInt32:(int32_t)value forKey:(NSString*)key {
	auto data = PBEncode(value);
	return [self setData:data forKey:key];
}

-(BOOL)setData:(NSData*)data forKey:(NSString*)key {
	auto kv = KV { key, data };
	auto buf = PBEncode(kv);
	return [self write:buf];
}

写入优化

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

空间增长

使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。需要在性能和空间上做个折中。

以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;

排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。

objectivec 复制代码
-(BOOL)append:(NSData*)data {
	if (space >= data.length) {
		append(fd, data);
	} else {
		newData = unique(m_allKV);
		if (total_space >= newData.length) {
			write(fd, newData);
		} else {
			while (total_space < newData.length) {
				total_space *= 2;
			}
			ftruncate(fd, total_space);
			write(fd, newData);
		}
	}
}

数据有效性

考虑到文件系统、操作系统都有一定的不稳定性, crc 校验,对无效数据进行甄别。在 iOS 微信现网环境上,有平均约 70万日次的数据校验不通过。

Android多进程访问

将 MMKV 迁移到 Android 平台之后,要支持多进程访问, iOS 不支持多进程。

  1. 性能:MMKV在性能方面表现更好。由于采用了内存映射技术,它可以直接在内存中读取和写入数据,减少了磁盘IO操作,因此读写速度更快。相比之下,SharedPreferences是基于XML文件存储的,读取和写入需要进行磁盘IO操作,速度较慢。

  2. 跨进程和跨线程支持:MMKV天然支持跨进程和跨线程的数据共享。多个进程或线程可以同时访问和修改MMKV中的数据,而无需额外的同步操作。而SharedPreferences的跨进程支持较差,需要进行额外的同步机制或使用ContentProvider等方式才能实现跨进程共享。

  3. 存储容量:MMKV支持更大的存储容量。SharedPreferences将所有数据都存储在一个XML文件中,如果数据较多,读取和解析整个文件可能会影响性能。而MMKV将数据划分为多个固定大小的内存页,可以高效地读取和写入大量数据。

  4. 序列化和加密:MMKV提供了数据的序列化和加密功能。它可以将复杂的数据结构序列化为字节数组进行存储,并支持对数据进行加密保护。而SharedPreferences只能存储基本数据类型,对于复杂的数据结构需要进行手动的序列化和反序列化操作。灵活性和易用性:MMKV提供了更灵活和易用的API。它的API设计更加简洁,使用起来更方便,支持链式调用和类型安全。同时,MMKV还提供了一些额外的功能,如数据的版本控制、数据迁移和备份等。

java 复制代码
import com.tencent.mmkv.MMKV;

public class MMKVExample {
    public static void main(String[] args) {
        // 初始化MMKV
        String rootDir = MMKV.initialize("path_to_directory");

        // 获取MMKV实例
        MMKV mmkv = MMKV.defaultMMKV();

        // 存储数据
        mmkv.putString("key1", "value1");
        mmkv.putInt("key2", 123);
        mmkv.putBoolean("key3", true);

        // 读取数据
        String value1 = mmkv.getString("key1", "");
        int value2 = mmkv.getInt("key2", 0);
        boolean value3 = mmkv.getBoolean("key3", false);

        System.out.println("Value 1: " + value1);
        System.out.println("Value 2: " + value2);
        System.out.println("Value 3: " + value3);
    }
}
java 复制代码
dependencies {
    implementation 'com.tencent:mmkv:1.0.23'
    // replace "1.0.23" with any available version
}

支持从SP迁移数据importFromSharedPreferences

MMKV 还额外实现了一遍 SharedPreferences、SharedPreferences.Editor 这两个 interface。

java 复制代码
// 可以跟SP用法一样
SharedPreferences.Editor editor = mmkv.edit();
// 无需调用 commit()
//editor.commit();

MMKV 的使用非常简单,所有变更立马生效,无需调用 sync、apply。 在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),例如在 MainActivity 里:

java 复制代码
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    String rootDir = MMKV.initialize(this);
    System.out.println("mmkv root: " + rootDir);
    //......
}

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

java 复制代码
import com.tencent.mmkv.MMKV;
//......

MMKV kv = MMKV.defaultMMKV();

kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");

kv.encode("int", Integer.MIN_VALUE);
int iValue = kv.decodeInt("int");

kv.encode("string", "Hello from mmkv");
String str = kv.decodeString("string");

使用后:

java 复制代码
public native void clearAll();

    // MMKV's size won't reduce after deleting key-values
    // call this method after lots of deleting f you care about disk usage
    // note that `clearAll` has the similar effect of `trim`
    public native void trim();

    // call this method if the instance is no longer needed in the near future
    // any subsequent call to the instance is undefined behavior
    public native void close();

    // call on memory warning
    // any subsequent call to the instance will load all key-values from file again
    public native void clearMemoryCache();

    // you don't need to call this, really, I mean it
    // unless you care about out of battery
    public void sync() {
        sync(true);
    }

限制

一个键会存入多分实例,最后存入的就是最新的。

MMKV 在大部分情况下都性能强劲,key/value 的数量和长度都没有限制。

然而 MMKV 在内存里缓存了所有的 key-value,在总大小比较大的情况下(例如 100M+),App 可能会爆内存,触发重整回写时,写入速度也会变慢。

锁 lock unlock tryLock

注意如果一个进程lock住,另一个进程mmkvWithID获取MMKV时就阻塞住,直到持有进程释放。

java 复制代码
// get the lock immediately
        MMKV mmkv2 = MMKV.mmkvWithID(LOCK_PHASE_2, MMKV.MULTI_PROCESS_MODE);
        mmkv2.lock();
        Log.d("locked in child", LOCK_PHASE_2);

        Runnable waiter = new Runnable() {
            @Override
            public void run() {
                //阻塞住 直到其他进程释放
                MMKV mmkv1 = MMKV.mmkvWithID(LOCK_PHASE_1, MMKV.MULTI_PROCESS_MODE);
                mmkv1.lock();
                Log.d("locked in child", LOCK_PHASE_1);
            }
        };

如果其他进程有进行修改,不会立即触发onContentChangedByOuterProcess,

checkLoadData如果变化,会clearMemoryState,重新loadFromFile。//数据量大时不要太频繁

读取decodeXXX会阻塞住,先回调onContentChangedByOuterProcess,再返回值,保证值是最新的。

Binder MMAP

Binder MMAP是Android系统中的一种机制,用于在跨进程通信(IPC)中传输大型数据或共享内存区域。

在Android中,进程间通信主要通过Binder框架实现。Binder框架使用Binder驱动程序在不同的进程之间建立通信通道。通常情况下,进程间通信是通过传输小型的数据结构,如整数、字符串等。然而,当需要传输大量数据或者共享内存时,效率会受到限制。

为了解决这个问题,Android引入了Binder MMAP机制。Binder MMAP允许进程通过内存映射(MMAP)的方式共享内存区域,从而实现高效的数据传输。它通过以下步骤实现:

  1. 发送端将数据写入内存区域:发送端将要传输的数据写入一个内存区域,该内存区域通过MMAP映射到物理内存中。

  2. 发送端将内存区域的描述符发送给接收端:发送端将内存区域的描述符(文件描述符)通过Binder传递给接收端。

  3. 接收端获取内存区域描述符并映射到自己的地址空间:接收端通过Binder接收内存区域的描述符,并将其映射到自己的地址空间。

  4. 接收端从内存区域读取数据:接收端可以直接从内存区域中读取发送端写入的数据,而无需进行数据拷贝。

Binder MMAP的优点:

  1. 高效的数据传输:通过内存映射的方式,避免了数据拷贝和序列化/反序列化操作,提高了数据传输的效率。

  2. 支持大型数据和共享内存:适用于传输或共享大量数据或大型内存区域的场景,可以减少内存占用和提高性能。

  3. 跨进程通信:作为Android的进程间通信机制,支持不同进程之间的数据传输,方便实现跨进程功能。

Binder MMAP的缺点:

  1. 复杂性:相比较其他传输方式,使用Binder MMAP需要更多的代码和配置,对开发者来说可能需要更多的学习和理解。

  2. 依赖Binder框架:作为Android系统的一部分,使用Binder MMAP需要依赖于Binder框架,需要遵循Binder框架的规范和约束。

MMKV的优点:

  1. 高性能:相对于SharedPreferences等传统存储方式,MMKV具有更好的读写性能,特别是在高并发操作下表现更出色。

  2. 跨进程支持:MMKV支持跨进程访问,多个进程可以同时读写同一个MMKV实例,方便实现进程间数据共享。

  3. 功能丰富:MMKV提供了丰富的功能,如加密、压缩等,可以满足不同的数据存储需求。

MMKV的缺点:

  1. 存储大小限制:MMKV存储的总大小受限于设备的存储空间,如果存储的数据量较大,可能会占用较多的存储空间。

  2. 适用性受限:由于MMKV是针对键值存储而设计的,适用于存储简单的键值对数据,不适合存储复杂的数据结构。

比sp快的原因

  1. 内存映射(Memory Mapping):MMKV使用了内存映射的技术,将数据直接映射到内存中,而不是像SharedPreferences一样将数据写入磁盘文件。这种内存映射的方式避免了频繁的磁盘读写操作,减少了IO开销,从而提高了读写性能。

  2. 零拷贝(Zero-copy):MMKV利用了内存映射的特性,实现了零拷贝的读写操作。当读取或写入数据时,MMKV直接在内存中进行操作,避免了数据的拷贝和序列化/反序列化操作,进一步提高了读写性能。

  3. 文件锁(File Locking):MMKV使用文件锁机制来保证多个进程对同一个MMKV实例的安全访问。这种文件锁机制可以有效地控制并发访问,避免了数据冲突和竞争条件,提高了并发操作的性能。

  4. 自定义序列化(Custom Serialization):MMKV使用自定义的序列化方式,将数据以二进制的形式存储,而不是SharedPreferences中的XML格式。这种自定义的序列化方式更加高效,减少了存储和解析数据的开销,提高了读写性能。

MMKV在android中的使用(替换sp)

在项目根目录下的 build.gradle 文件中加入

java 复制代码
dependencies {
    implementation 'com.tencent:mmkv-static:1.2.10'
}

在项目 app 模块下的 build.gradle 文件中加入

java 复制代码
buildscript {
    repositories {
        mavenCentral()//这行依赖
    }
}
allprojects {
    repositories {
        mavenCentral()//这行依赖
    }
}

在Application中初始化

java 复制代码
MMKV.initialize(this)

MMKV 默认把文件存放在$(FilesDir)/mmkv/目录。你可以在 MMKV初始化时自定义根目录:

java 复制代码
String dir = getFilesDir().getAbsolutePath() + "/mmkv";
String rootDir = MMKV.initialize(dir);

Kotlin中使用

Kotlin 复制代码
import com.tencent.mmkv.MMKV;
//......

//1. 获取默认全局实例 (与下面的几选一,一般就使用这个就行)
var mmkv: MMKV = MMKV.defaultMMKV()

//2. 也可以自定义MMKV对象,设置自定ID  (根据业务区分的存取实例)
var mmkv: MMKV = MMKV.mmkvWithID("ID")

//3. MMKV默认是支持单进程的,如果业务需要多进程访问,需要在初始化的时候添加多进程模式参数
var mmkv = MMKV.mmkvWithID("ID", MMKV.MULTI_PROCESS_MODE)  //多进程同步支持

存取方法

java 复制代码
// 添加/更新数据
mmkv?.encode(key, value);

// 获取数据
int value = mmkv.decodeInt(key);
String value = mmkv.decodeString(key);
//...获取等类型

// 删除数据
mmkv.removeValueForKey(key);

如果需要存取对象,可以用存取对象json字符串的方法,将对象转成json存,取出json转回对象。

SP迁移

MMKV可以调用importFromSharedPreferences方法进行SP的数据迁移,示例代码如下: MMKV实现了SharedPreferences,Editor两个接口,所以在迁移之后SP的操作代码可以不用更改。

java 复制代码
val mmkv = MMKV.mmkvWithID("myData")
val olderData = DemoApplication.mContext?.getSharedPreferences("myData", MODE_PRIVATE)
mmkv?.importFromSharedPreferences(olderData)
olderData?.edit()?.clear()?.apply()

Java中的使用

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

java 复制代码
import com.tencent.mmkv.MMKV;
//......

//1. 获取默认全局实例 (一般就使用这个就行)
MMKV kv = MMKV.defaultMMKV();

//2. 也可以自定义MMKV对象,设置自定ID  (根据业务区分的存取实例)
MMKV kv = MMKV.mmkvWithID("ID");

//3. MMKV默认是支持单进程的,如果业务需要多进程访问,需要在初始化的时候添加多进程模式参数
MMKV kv = MMKV.mmkvWithID("ID", MMKV.MULTI_PROCESS_MODE); //多进程同步支持

存取方法

java 复制代码
/** 添加/更新数据 **/
//存boolean类型
kv.encode("bool", true);
//存int类型
kv.encode("int", Integer.MIN_VALUE);
//存string类型
kv.encode("string", "MyiSMMKV");


/** 获取数据 **/
//获取boolean类型数据
boolean bValue = kv.decodeBool("bool");
//获取int类型数据
int iValue = kv.decodeInt("int");
//获取string类型数据
String str = kv.decodeString("string");
//...等类型的获取

// 删除数据
mmkv.removeValueForKey(key);

如果需要存取对象,可以用存取对象json字符串的方法,将对象转成json存,取出json转回对象。

SP迁移

java 复制代码
MMKV kv = MMKV.mmkvWithID("myData");
SharedPreferences olderData = App.getInstance().getSharedPreferences("myData", MODE_PRIVATE);
kv.importFromSharedPreferences(olderData);
olderData.edit().clear().apply();
相关推荐
nothingbutluck46414 分钟前
2025.4.10 html有序、无序、定义列表、音视频标签
前端·html·音视频
Yang-Never18 分钟前
Git -> Git使用Patch失败error: patch failed: patch does not apply的解决办法
android·git·android studio
Cao12345678932119 分钟前
扫雷-C语言版
c语言·开发语言
天堂的恶魔94630 分钟前
QT —— 信号和槽(槽函数)
开发语言·qt
牛马baby34 分钟前
Springboot 自动装配原理是什么?SPI 原理又是什么?
java·spring boot·后端
水w36 分钟前
【Python爬虫】详细入门指南
开发语言·爬虫·python·scrapy·beautifulsoup
爱上python的猴子1 小时前
chrome中的copy xpath 与copy full xpath的区别
前端·chrome
小小深1 小时前
了解JVM
java·jvm
Sunlight_7771 小时前
第五章 SQLite数据库:1、SQLite 基础语法及使用案例
java·linux·服务器·jvm·数据库·tcp/ip·sqlite
JhonKI1 小时前
【从零实现高并发内存池】内存池整体框架设计 及 thread cache实现
java·redis·缓存