MMKV
MoMo Lv5

MMKV是什么

MMKV 是基于 mmap (memory mapping)内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。

简单来说,它就是一个【可以用于替代SP】,操作与SP类似的存储组件。

内存映射的定义

内存映射(Memory Mapping)是一种将文件内容映射到进程的虚拟地址空间的技术。在这种机制下,文件可以被视为内存的一部分,从而允许程序直接对这部分内存进行读写操作,而无需传统的文件 I/O 调用。这种方法不仅简化了文件操作,还提高了处理效率。

mmap 系统调用概述

mmap 是实现内存映射的关键系统调用。它创建了文件内容和进程地址空间之间的直接映射,使得文件的一部分或全部可以直接映射到进程的地址空间中。这样,文件的读写就变得像内存访问一样高效。

mmap 系统调用在 Linux 和类 Unix 系统中提供了内存映射的功能。它允许程序员将整个文件或文件的一部分映射到进程的地址空间。通过这种方式,文件内容可以通过指针直接访问,就像访问普通的内存数组一样,这极大地提高了文件操作的效率和直观性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/mman.h>
#include <fcntl.h>
void *map_file(const char *filepath, size_t size) {
int fd = open(filepath, O_RDONLY);
if (fd == -1) {
// 处理打开文件的错误
return NULL;
}
void *mapped = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
// 处理映射失败的错误
close(fd);
return NULL;
}
close(fd);
return mapped;
}

在这段代码中,打开了一个文件并使用 mmap 将其映射到内存。直接通过返回的指针来访问文件内容,而不需要进行传统的文件读写操作。

SharedPreference缺陷

读写效率低

主要原因是其本身的读写方式导致的:

读写方式:I/O
数据格式:xml
写入方式:全量更新

即每当需要更新一项数据,SharedPreferences的整个读写过程都是:将所有数据转化成xml格式 -> 通过I/O方式写入。

写文件流程:
1、调用write向内核发起系统调用,上下文从用户态切换为内核态;
2、CPU 将用户缓冲区中的数据拷贝到内核空间的缓冲区,(CPU拷贝);
3、CPU 利用 DMA控制器将数据从内核缓冲区拷贝到磁盘缓冲区进行数据传输。(DMA拷贝);
4、上下文从内核态切换回用户态,write 系统调用执行返回

容易导致ANR

主要是由于同步提交(commit)、异步提交(Apply) 和 获取数据getXX()导致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*
* 1. 同步提交commit
* commit提交是同步的,直到磁盘操作成功后才会完成
* 所以当数据量比较大时,使用commit很可能引起ANR
*/
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}

/*
* 回调的时机:
* 1. commit是在内存和硬盘操作均结束时回调
* 2. apply是内存操作结束时就进行回调
*/
notifyListeners(mcr);
return mcr.writeToDiskResult;

}

/*
* 2. 异步提交apply
* 当数据量比较大时,使用apply也可能引起ANR
*/
public void apply() {
final long startTime = System.currentTimeMillis();

final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {

// 启用等待
mcr.writtenToDiskLatch.await();
......
}
};

// 将 awaitCommit 添加到队列 QueuedWork 中
QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 将写入任务加入到队列中,而写入任务在一个线程中执行
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

// 为了保证异步任务及时完成,当生命周期处于 handleStopService() 、handlePauseActivity() 、handleStopActivity()时会调用QueuedWork.waitToFinish() 会等待写入任务执行完毕
// waitToFinish() :会一直等待写入任务执行完毕,其它什么都不做。
// 当有很多写入任务,会依次执行;当文件很大时,效率很低,则容易造成 ANR
public static void waitToFinish() {
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}

/*
* 3. 获取数据getXX()
* 所有 getXXX() 方法都是同步的,在主线程调用 get 方法,必须等待 SP 加载完毕,也有可能导致ANR
*/

// 使用getSharedPreferences() 最终会调用SharedPreferencesImpl#startLoadFromDisk()开启一个线程异步读取数据
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();

// 当我们正在读取一个比较大的数据,还没读取完,接着调用 getXXX()。
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
// 在同步方法内调用了wait(),会一直等待 getSharedPreferences() 开启的线程读取完数据才能继续往下执行
// 如果读取一个大的文件,也很大可能会造成ANR
private void awaitLoadedLocked() {
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
}

MMKV性能优势

MMAP对文件的读写操作只需要从磁盘到用户主存的一次数据拷贝过程,减少了数据的拷贝次数,提高了文件操作效率。

MMAP使用逻辑内存对磁盘文件进行映射,操作内存就相当于操作文件,不需要开启线程,操作MMAP的速度和操作内存的速度一样快;

MMAP提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统如内存不足、进程退出等时候负责将内存回写到文件。

image

支持的数据类型:

  1. 支持以下 Java 语言基础类型:
  • boolean、int、long、float、double、byte[]
  1. 支持以下 Java 类和容器:
  • String、Set
  • 任何实现了Parcelable的类型
  1. 数据组织
    数据序列化方面选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。

  2. 写入优化
    考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。

  3. 空间增长
    使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
    无法处理输入事件。常见的原因包括主线程中进行了耗时的I/O操作、复杂的计算、长时间的等待等。

MMKV的使用

初始化以及修改根目录

1
2
3
4
MMKV.initialize(this)//(与下面的几选一,一般就使用这个就行)

String dir = getFilesDir().getAbsolutePath() + "/mmkv";
String rootDir = MMKV.initialize(dir);

获取MMKV实例

1
2
3
4
5
6
7
8
9
10
11
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); //多进程同步支持

存取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 添加/更新数据 **/
//存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);

MMKV原理

对文件进行mmap,会在进程的虚拟内存分配地址空间创建映射关系。
实现这样的映射关系后,就可以采用指针的方式读写操作这一段内存,而系统会自动回写到对应的文件磁盘上

少了把数据传给内核,再由内核存到文件的步骤,不需要内核作为中转,可以直接在用户控件对文件进行读写,少了一次CPU的拷贝

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

mmap 是一种在 Linux 和其他类 Unix 操作系统中使用的系统调用,它允许程序将一个文件或者设备映射到内存中。这样做的好处是可以直接通过内存操作来访问文件内容,而不需要使用传统的 read 和 write 系统调用。mmap 提供了一种高效的方式来处理文件数据,特别是在需要频繁读写大文件的场景下。

  • 内存映射:mmap 将文件内容映射到进程的地址空间,使得文件内容看起来就像是内存中的一部分。

  • 高效访问:由于文件内容被映射到内存,所以访问文件数据就像访问内存一样快。

  • 共享内存:使用 mmap 可以创建共享内存区域,这对于进程间通信(IPC)非常有用。

  • 自动同步:对映射区域的修改会自动同步回文件,无需显式调用 write 操作。

  • 内存管理:mmap 可以帮助操作系统更好地管理内存,因为它允许操作系统在需要时将文件内容从物理内存中换出到磁盘。

  • 文件锁定:mmap 可以用于文件锁定,防止其他进程修改文件。

  • 支持匿名映射:除了文件映射外,mmap 还支持创建匿名映射,这种映射不与任何文件关联,通常用于动态内存分配。

mmap 系统调用和直接使用IPC共享内存之间的差异

mmap 系统调用用于将文件映射到进程的地址空间中,而共享内存是一种不同的机制,用于进程间通信。这两种方法都用于数据共享和高效的内存访问,但它们有一些关键区别:

1. 数据源和持久化:

  • mmap: 通过 mmap 映射的数据通常来自文件系统中的文件。这意味着数据是持久化的——即使程序终止,文件中的数据依然存在。当你通过映射的内存区域修改数据时,这些更改最终会反映到磁盘上的文件中。

  • 共享内存:共享内存是一块匿名的(或者有时与特定文件关联的)内存区域,它可以被多个进程访问。与 mmap 映射的文件不同,共享内存通常是非持久的,即数据仅在计算机运行时存在,一旦系统关闭或重启,存储在共享内存中的数据就会丢失。

2. 使用场景:

  • mmap:mmap 特别适合于需要频繁读写大文件的场景,因为它可以减少磁盘 I/O 操作的次数。它也允许文件的一部分被映射到内存中,这对于处理大型文件尤为有用。
  • 共享内存:共享内存通常用于进程间通信(IPC),允许多个进程访问相同的内存区域,这样可以非常高效地在进程之间交换数据。

3. 性能和效率:

  • mmap:映射文件到内存可以提高文件访问的效率,尤其是对于随机访问或频繁读写的场景。系统可以利用虚拟内存管理和页面缓存机制来优化访问。

  • 共享内存:共享内存提供了一种非常快速的数据交换方式,因为所有的通信都在内存中进行,没有文件 I/O 操作。

4. 同步和一致性:

  • mmap:使用 mmap 时,必须考虑到文件内容的同步问题。例如,使用 msync 调用来确保内存中的更改被同步到磁盘文件中。

  • 共享内存:在共享内存的环境中,进程需要使用某种形式的同步机制(如信号量、互斥锁)来避免竞争条件和数据不一致。

mmap 和页缓存

页缓存涉及情况

  • 当使用 mmap 映射文件到内存时,操作系统利用页缓存来优化对这些文件数据的访问。页缓存是操作系统的一部分,用于存储从磁盘读取的数据页。

  • 访问 mmap 映射的文件时,并不是每次读取都会直接触及磁盘。如果所需数据已经在页缓存中(由于之前的读取操作),则直接从缓存中获取数据,而不需要磁盘 I/O。

物理内存页

  • 无论是 mmap 映射的文件还是共享内存,最终都是以物理内存页的形式存在。操作系统通过管理这些内存页来控制程序的内存访问。

共享内存和内存页

1. 共享内存也在内存页中:

  • 共享内存确实也是在物理内存页中分配的。当我们谈论共享内存没有“缓存”,是指它不依赖于磁盘的页缓存,因为共享内存不是基于文件的。

2. 实时性和磁盘 I/O:

  • 共享内存的实时性体现在它提供了一种直接访问物理内存的方式,而无需经过文件系统或磁盘 I/O。

  • 对于共享内存,一旦数据被写入内存,它就立即对所有共享该内存区域的进程可见,没有额外的读取延迟。

总结

  • mmap 和共享内存都使用物理内存页。不同之处在于 mmap 通常用于映射文件到内存,因此它与磁盘的页缓存密切相关,可以减少对磁盘的访问。

  • 共享内存不涉及文件系统或磁盘 I/O,因此提供了更快速、更直接的内存访问,这在需要极低通信延迟的应用中非常重要。

mmap 与文件 I/O

传统文件 I/O 的局限性

传统文件 I/O 操作,比如 read 和 write,虽然直观易懂,但它们有一定的局限性。每次读写操作都需要从用户空间切换到内核空间,这导致额外的上下文切换开销,特别是在频繁的小规模读写操作中,这种开销尤为明显。

效率比较

功能 传统文件I/O mmap
数据访问 通过系统调用进行读写 直接内存访问
CPU开销 高(频繁的用户态/内核态切换) 低(减少上下文切换)
适用场景 小文件或不频繁访问的文件 大文件或频繁访问的文件
内存效率 一般 高(利用页面缓存)
Powered by Hexo & Theme Keep
Unique Visitor Page View