MMKV(1)

  • 内存准备

通过 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万日次的数据校验不通过。

  • 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还提供了一些额外的功能,如数据的版本控制、数据迁移和备份等。

  5. 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

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 文件中加入

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()
  • Java中的使用

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();

你可能感兴趣的:(Android,java,开发语言,前端,android,android-studio)