通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
数据序列化方面选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。考虑到要提供的是通用 kv 组件,key 可以限定是 string 字符串类型,value 则多种多样(int/bool/double 等)。要做到通用的话,考虑将 value 通过 protobuf 协议序列化成统一的内存块(buffer),然后就可以将这些 KV 对象序列化到内存中。
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 排重,尝试序列化保存排重结果;
排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。
-(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万日次的数据校验不通过。
将 MMKV 迁移到 Android 平台之后,要支持多进程访问, iOS 不支持多进程。
性能:MMKV在性能方面表现更好。由于采用了内存映射技术,它可以直接在内存中读取和写入数据,减少了磁盘IO操作,因此读写速度更快。相比之下,SharedPreferences是基于XML文件存储的,读取和写入需要进行磁盘IO操作,速度较慢。
跨进程和跨线程支持:MMKV天然支持跨进程和跨线程的数据共享。多个进程或线程可以同时访问和修改MMKV中的数据,而无需额外的同步操作。而SharedPreferences的跨进程支持较差,需要进行额外的同步机制或使用ContentProvider等方式才能实现跨进程共享。
存储容量:MMKV支持更大的存储容量。SharedPreferences将所有数据都存储在一个XML文件中,如果数据较多,读取和解析整个文件可能会影响性能。而MMKV将数据划分为多个固定大小的内存页,可以高效地读取和写入大量数据。
序列化和加密:MMKV提供了数据的序列化和加密功能。它可以将复杂的数据结构序列化为字节数组进行存储,并支持对数据进行加密保护。而SharedPreferences只能存储基本数据类型,对于复杂的数据结构需要进行手动的序列化和反序列化操作。灵活性和易用性:MMKV提供了更灵活和易用的API。它的API设计更加简洁,使用起来更方便,支持链式调用和类型安全。同时,MMKV还提供了一些额外的功能,如数据的版本控制、数据迁移和备份等。
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);
}
}
dependencies {
implementation 'com.tencent:mmkv:1.0.23'
// replace "1.0.23" with any available version
}
支持从SP迁移数据importFromSharedPreferences
MMKV 还额外实现了一遍 SharedPreferences、SharedPreferences.Editor 这两个 interface。
// 可以跟SP用法一样
SharedPreferences.Editor editor = mmkv.edit();
// 无需调用 commit()
//editor.commit();
MMKV 的使用非常简单,所有变更立马生效,无需调用 sync、apply。 在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),例如在 MainActivity 里:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String rootDir = MMKV.initialize(this);
System.out.println("mmkv root: " + rootDir);
//……
}
MMKV 提供一个全局的实例,可以直接使用:
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");
使用后:
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时就阻塞住,直到持有进程释放。
// 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是Android系统中的一种机制,用于在跨进程通信(IPC)中传输大型数据或共享内存区域。
在Android中,进程间通信主要通过Binder框架实现。Binder框架使用Binder驱动程序在不同的进程之间建立通信通道。通常情况下,进程间通信是通过传输小型的数据结构,如整数、字符串等。然而,当需要传输大量数据或者共享内存时,效率会受到限制。
为了解决这个问题,Android引入了Binder MMAP机制。Binder MMAP允许进程通过内存映射(MMAP)的方式共享内存区域,从而实现高效的数据传输。它通过以下步骤实现:
发送端将数据写入内存区域:发送端将要传输的数据写入一个内存区域,该内存区域通过MMAP映射到物理内存中。
发送端将内存区域的描述符发送给接收端:发送端将内存区域的描述符(文件描述符)通过Binder传递给接收端。
接收端获取内存区域描述符并映射到自己的地址空间:接收端通过Binder接收内存区域的描述符,并将其映射到自己的地址空间。
接收端从内存区域读取数据:接收端可以直接从内存区域中读取发送端写入的数据,而无需进行数据拷贝。
Binder MMAP的优点:
高效的数据传输:通过内存映射的方式,避免了数据拷贝和序列化/反序列化操作,提高了数据传输的效率。
支持大型数据和共享内存:适用于传输或共享大量数据或大型内存区域的场景,可以减少内存占用和提高性能。
跨进程通信:作为Android的进程间通信机制,支持不同进程之间的数据传输,方便实现跨进程功能。
Binder MMAP的缺点:
复杂性:相比较其他传输方式,使用Binder MMAP需要更多的代码和配置,对开发者来说可能需要更多的学习和理解。
依赖Binder框架:作为Android系统的一部分,使用Binder MMAP需要依赖于Binder框架,需要遵循Binder框架的规范和约束。
MMKV的优点:
高性能:相对于SharedPreferences等传统存储方式,MMKV具有更好的读写性能,特别是在高并发操作下表现更出色。
跨进程支持:MMKV支持跨进程访问,多个进程可以同时读写同一个MMKV实例,方便实现进程间数据共享。
功能丰富:MMKV提供了丰富的功能,如加密、压缩等,可以满足不同的数据存储需求。
MMKV的缺点:
存储大小限制:MMKV存储的总大小受限于设备的存储空间,如果存储的数据量较大,可能会占用较多的存储空间。
适用性受限:由于MMKV是针对键值存储而设计的,适用于存储简单的键值对数据,不适合存储复杂的数据结构。
内存映射(Memory Mapping):MMKV使用了内存映射的技术,将数据直接映射到内存中,而不是像SharedPreferences一样将数据写入磁盘文件。这种内存映射的方式避免了频繁的磁盘读写操作,减少了IO开销,从而提高了读写性能。
零拷贝(Zero-copy):MMKV利用了内存映射的特性,实现了零拷贝的读写操作。当读取或写入数据时,MMKV直接在内存中进行操作,避免了数据的拷贝和序列化/反序列化操作,进一步提高了读写性能。
文件锁(File Locking):MMKV使用文件锁机制来保证多个进程对同一个MMKV实例的安全访问。这种文件锁机制可以有效地控制并发访问,避免了数据冲突和竞争条件,提高了并发操作的性能。
自定义序列化(Custom Serialization):MMKV使用自定义的序列化方式,将数据以二进制的形式存储,而不是SharedPreferences中的XML格式。这种自定义的序列化方式更加高效,减少了存储和解析数据的开销,提高了读写性能。
在项目根目录下的 build.gradle 文件中加入
dependencies {
implementation 'com.tencent:mmkv-static:1.2.10'
}
在项目 app 模块下的 build.gradle 文件中加入
buildscript {
repositories {
mavenCentral()//这行依赖
}
}
allprojects {
repositories {
mavenCentral()//这行依赖
}
}
在Application中初始化
MMKV.initialize(this)
MMKV 默认把文件存放在$(FilesDir)/mmkv/目录。你可以在 MMKV初始化时自定义根目录:
String dir = getFilesDir().getAbsolutePath() + "/mmkv";
String rootDir = MMKV.initialize(dir);
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) //多进程同步支持
存取方法
// 添加/更新数据
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的操作代码可以不用更改。
val mmkv = MMKV.mmkvWithID("myData")
val olderData = DemoApplication.mContext?.getSharedPreferences("myData", MODE_PRIVATE)
mmkv?.importFromSharedPreferences(olderData)
olderData?.edit()?.clear()?.apply()
MMKV 提供一个全局的实例,可以直接使用
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); //多进程同步支持
存取方法
/** 添加/更新数据 **/
//存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迁移
MMKV kv = MMKV.mmkvWithID("myData");
SharedPreferences olderData = App.getInstance().getSharedPreferences("myData", MODE_PRIVATE);
kv.importFromSharedPreferences(olderData);
olderData.edit().clear().apply();