找回密码
 立即注册
查看: 421|回复: 0

MMKV 高性能的数据存取框架解读

[复制链接]
发表于 2021-10-25 19:12 | 显示全部楼层 |阅读模式
MMKV

目标


了解MMKV

MMKV的基本应用

MMKV的原理概念

多进程设计思想

性能对比

源码解读
简介


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

官方文档:https://github.com/Tencent/MMKV/blob/master/README_CN.md

项目地址:https://github.com/Tencent/MMKV
mmap

简单解释(仅供参考)


把文件描述符fd(部分硬件资源外存统一描述符)映射到虚拟空间中,所以能够实现进程间的通信、数据存取。


image-20210819112108803.png

映射流程(仅供参考)


1、用户进程调用内存映射函数库mmap,当前进程在虚拟地址空间中,寻找一段空闲的满足要求的虚拟地址

2、此时内核收到相关请求后会调用内核的mmap函数,注意,不同于用户空间库函数。内核mmap函数通过虚拟文件系统定位到文件磁盘物理地址,既实现了文件地址和虚拟地址区域的映射关系。 此时,这片虚拟地址并没有任何数据关联到主存中。

注意,前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

3、进程的读或写操作访问虚拟地址空间这一段映射地址,现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页中断。

4、由于引发了缺页中断,内核则调用nopage函数把所缺的页从磁盘装入到主存中

5、之后用户进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
应用


Linux进程的创建

Android Binder

微信MMKV组件

美团Logan

参考文章

Android-内存映射mmap

mmap的理解

Android应用使用mmap实例
ProtoBuf

简介


protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。

更多内容、实际应用可参考官方文档。

官方文档:https://developers.google.com/protocol-buffers/docs/overview
特性


语言无关、平台无关:即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台

高效:即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单

扩展性、兼容性好:你可以更新数据结构,而不影响和破坏原有的旧程序
数据结构


20191225165541255.png

时间效率对比:
数据格式1000条数据5000条数据
Protobuf195ms647ms
Json515ms2293ms

空间效率对比:
数据格式5000条数据
Protobuf22MB
Json29MB

参考文章

https://www.jianshu.com/p/73c9ed3a4877
https://www.jianshu.com/p/a24c88c0526a
简单使用


MMKV 的使用非常简单,所有变更立马生效,无需调用 sync、apply。
依赖

dependencies {    implementation 'com.tencent:mmkv:1.0.10'    // replace "1.0.10" with any available version}初始化


配置 MMKV 根目录

在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),例如在 Application 里:
public void onCreate() {    super.onCreate();    String rootDir = MMKV.initialize(this);    System.out.println("mmkv root: " + rootDir);     //data/user/0包名/files/mmkv}
其他初始化的方法
//指定日志级别initialize(Context context, MMKVLogLevel logLevel)//指定存储地址和日志级别initialize(String rootDir)initialize(String rootDir, MMKVLogLevel logLevel)//MMKV.LibLoader用来解决Android 设备(API level 19)在安装/更新 APK 时出错问题initialize(String rootDir, MMKV.LibLoader loader)initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel)CRUD 操作


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");删除 & 查询

MMKV kv = MMKV.defaultMMKV();kv.removeValueForKey("bool");System.out.println("bool: " + kv.decodeBool("bool"));    kv.removeValuesForKeys(new String[]{"int", "long"});System.out.println("allKeys: " + Arrays.toString(kv.allKeys()));boolean hasBool = kv.containsKey("bool");区分存储


使用MMKV.mmkvWithID即可创建不同的存储区域的MMKV实例。
MMKV kv = MMKV.mmkvWithID("MyID");kv.encode("bool", true);支持的数据类型


    支持以下 Java 语言基础类型:
      boolean、int、long、float、double、byte[]

    支持以下 Java 类和容器:
      String、Set<String>任何实现了Parcelable的类型

SharedPreferences 迁移

    MMKV 提供了 importFromSharedPreferences() 函数,可以比较方便地迁移数据过来
/** * An highly efficient, reliable, multi-process key-value storage framework. * THE PERFECT drop-in replacement for SharedPreferences and MultiProcessSharedPreferences. */public class MMKV implements SharedPreferences, SharedPreferences.Editor {
    MKV 还额外实现了一遍 SharedPreferences、SharedPreferences.Editor 这两个 interface,在迁移的时候只需两三行代码即可,其他 CRUD 操作代码都不用改。
private void testImportSharedPreferences() {    //SharedPreferences preferences = getSharedPreferences("myData", MODE_PRIVATE);    MMKV preferences = MMKV.mmkvWithID("myData");    // 迁移旧数据    {        SharedPreferences old_man = getSharedPreferences("myData", MODE_PRIVATE);        preferences.importFromSharedPreferences(old_man);        old_man.edit().clear().commit();    }    // 跟以前用法一样    SharedPreferences.Editor editor = preferences.edit();    editor.putBoolean("bool", true);    editor.putInt("int", Integer.MIN_VALUE);    editor.putLong("long", Long.MAX_VALUE);    editor.putFloat("float", -3.14f);    editor.putString("string", "hello, imported");    HashSet<String> set = new HashSet<String>();    set.add("W"); set.add("e"); set.add("C"); set.add("h"); set.add("a"); set.add("t");    editor.putStringSet("string-set", set);    // 无需调用 commit()    //editor.commit();}进阶使用

日志


日志切面AOP思想

MMKV 默认将日志打印到 logcat,不便于对线上问题进行定位和解决。你可以在 App 启动时接收转发 MMKV 的日志。实现MMKVHandler接口,添加类似下面的代码:
@Overridepublic boolean wantLogRedirecting() {    return true;}@Overridepublic void mmkvLog(MMKVLogLevel level, String file, int line, String func, String message) {    String log = "<" + file + ":" + line + "::" + func + "> " + message;    switch (level) {        case LevelDebug:            //Log.d("redirect logging MMKV", log);            break;        case LevelInfo:            //Log.i("redirect logging MMKV", log);            break;        case LevelWarning:            //Log.w("redirect logging MMKV", log);            break;        case LevelError:            //Log.e("redirect logging MMKV", log);            break;        case LevelNone:            //Log.e("redirect logging MMKV", log);            break;    }}
如果你不希望 MMKV 打印日志,你可以关掉它(虽然我们强烈不建议你这么做)。
注意:除非有非常强烈的证据表明MMKV的日志拖慢了App的速度,你不应该关掉日志。没有日志,日后万一用户有问题,将无法跟进。
MMKV.setLogLevel(MMKVLogLevel.LevelNone);加密


MMKV 默认明文存储所有 key-value,依赖 Android 系统的沙盒机制保证文件加密。如果你担心信息泄露,你可以选择加密 MMKV。
String cryptKey = "My-Encrypt-Key";MMKV kv = MMKV.mmkvWithID("MyID", MMKV.SINGLE_PROCESS_MODE, cryptKey);
你可以更改密钥,也可以将一个加密 MMKV 改成明文,或者反过来。
final String mmapID = "testAES_reKey1";// an unencrypted MMKV instanceMMKV kv = MMKV.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, null);// change from unencrypted to encryptedkv.reKey("Key_seq_1");// change encryption keykv.reKey("Key_seq_2");// change from encrypted to unencryptedkv.reKey(null);自定义 library loader


一些 Android 设备(API level 19)在安装/更新 APK 时可能出错, 导致 libmmkv.so 找不到。然后就会遇到 java.lang.UnsatisfiedLinkError 之类的 crash。有个开源库 ReLinker 专门解决这个问题,你可以用它来加载 MMKV:
String dir = getFilesDir().getAbsolutePath() + "/mmkv";MMKV.initialize(dir, new MMKV.LibLoader() {    @Override    public void loadLibrary(String libName) {        ReLinker.loadLibrary(MyApplication.this, libName);    }});
Relinker简介:

本地库加载框架,github1000+的star

原理:

尝试使用系统原生方式去加载so,如果加载失败,Relinker会尝试从apk中拷贝so到App沙箱目录下,然后再去尝试加载so。最终,我们可以使用 ReLinker.loadLibrary(context, “mylibrary”) 来加载本地库。
Native Buffer


当从 MMKV 取一个 String or byte[]的时候,会有一次从 native 到 JVM 的内存拷贝。如果这个值立即传递到另一个 native 库(JNI),又会有一次从 JVM 到 native 的内存拷贝。当这个值比较大的时候,整个过程会非常浪费。Native Buffer 就是为了解决这个问题。
Native Buffer 是由 native 创建的内存缓冲区,在 Java 里封装成 NativeBuffer 类型,可以透明传递到另一个 native 库进行访问处理。整个过程避免了先拷内存到 JVM 又从 JVM 拷回来导致的浪费。示例代码:
int sizeNeeded = kv.getValueActualSize("bytes");NativeBuffer nativeBuffer = MMKV.createNativeBuffer(sizeNeeded);if (nativeBuffer != null) {    int size = kv.writeValueToNativeBuffer("bytes", nativeBuffer);    Log.i("MMKV", "size Needed = " + sizeNeeded + " written size = " + size);    // pass nativeBuffer to another native library    // ...    // destroy when you're done    MMKV.destroyNativeBuffer(nativeBuffer);}跨进程通信的实现


本质:共享MMKV实例化信息完成对象的伪复制

    通信的数据对象

    该类MMKV内部已经实现,传递进程A的mmkv信息给B进程,B进程新建MMKV实例,B就可以通过MMKV实例来完成数据的操作
public final class ParcelableMMKV implements Parcelable {    private final String mmapID;    private int ashmemFD = -1;    private int ashmemMetaFD = -1;    private String cryptKey = null;    public ParcelableMMKV(MMKV mmkv) {        mmapID = mmkv.mmapID();        ashmemFD = mmkv.ashmemFD();        ashmemMetaFD = mmkv.ashmemMetaFD();        cryptKey = mmkv.cryptKey();    }    private ParcelableMMKV(String id, int fd, int metaFD, String key) {        mmapID = id;        ashmemFD = fd;        ashmemMetaFD = metaFD;        cryptKey = key;    }    public MMKV toMMKV() {        if (ashmemFD >= 0 && ashmemMetaFD >= 0) {            return MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);        }        return null;    }}


  • Aidl文件,需要手动创建该文件
    import com.tencent.mmkv.ParcelableMMKV;interface IAshmemMMKV {    ParcelableMMKV GetAshmemMMKV();}
    Aidl定义了跨进程通信的方法细则,这里只需要一个get方法,返回ParcelableMMKV通信实体。

  • 服务端

    服务端Service
    public class UserServer extends Service {  @Nullable    @Override    public IBinder onBind(Intent intent) {          Log.i(TAG, "onBind, intent=" + intent);        return new AshmemMMKVGetter();    }}public class AshmemMMKVGetter extends IAshmemMMKV.Stub {    private AshmemMMKVGetter() {        // 1M, ashmem cannot change size after opened        final String id = "tetAshmemMMKV";        try {            m_ashmemMMKV = MMKV.mmkvWithAshmemID(BenchMarkBaseService.this, id, AshmemMMKV_Size,                    MMKV.MULTI_PROCESS_MODE, CryptKey);            m_ashmemMMKV.encode("bool", true);        } catch (Exception e) {            Log.e("MMKV", e.getMessage());        }    }    public ParcelableMMKV GetAshmemMMKV() {        return new ParcelableMMKV(m_ashmemMMKV);    }}
    客户端

    onServiceConnected连接之后
Intent intent = new Intent();intent.setAction("***.***.***");intent.setPackage("***.***.***");bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); private ServiceConnection serviceConnection = new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder service) {               IAshmemMMKV ashmemMMKV = IAshmemMMKV.Stub.asInterface(service);        try {            ParcelableMMKV parcelableMMKV = ashmemMMKV.GetAshmemMMKV();            if (parcelableMMKV != null) {                m_ashmemMMKV = parcelableMMKV.toMMKV();                if (m_ashmemMMKV != null) {                    Log.i("MMKV", "ashmem bool: " + m_ashmemMMKV.decodeBool("bool"));                }            }        } catch (RemoteException e) {            e.printStackTrace();        }        }        @Override        public void onServiceDisconnected(ComponentName name) {            isBind = false;                  }    }; 原理

内存准备


通过 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];}写入优化


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


使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。我们需要在性能和空间上做个折中:以内存 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万日次的数据校验不通过。
多进程设计思想


官网地址:https://github.com/Tencent/MMKV/wiki/android_ipc

官网有详细的说明,这里主要分享思想:

CS架构:

IPC CS架构有Binder、Socket等,特点是一个单独进程管理数据,数据同步不易出错,简单好用易上手,缺点是慢。

去中心化:

只需要将文件 mmap 到每个访问进程的内存空间,加上合适的进程锁,再处理好数据的同步,就能够实现多进程并发访问。
性能对比

单进程


读写效率
mmkvSharedPreferencessqlite
write int 10006.5693.1774.4
write String 100018.91003.9857.3
read int 10004.31.5302.9
read String 10008.31.3320.7

单进程性能
可见,MMKV 在写入性能上远远超越 SharedPreferences & SQLite,在读取性能上也有相近或超越的表现。


image-20210826111619059.png

(测试机器是 华为 Mate 20 Pro 128G,Android 10,每组操作重复 1k 次,时间单位是 ms。)
多进程性能


可见,MMKV 无论是在写入性能还是在读取性能,都远远超越 MultiProcessSharedPreferences & SQLite & SQLite。


image-20210826111725683.png

性能对比: https://github.com/Tencent/MMKV/wiki/android_benchmark_cn
原理上和SharedPreference区别

SharedPreference原理


本质是在本地磁盘记录了一个xml文件,在构造方法中开启一个子线程加载磁盘中的xml文件
@UnsupportedAppUsageprivate void startLoadFromDisk() {    synchronized (mLock) {        mLoaded = false;    }    new Thread("SharedPreferencesImpl-load") {        public void run() {            loadFromDisk();        }    }.start();}
SharedPreferencesImpl内部维护Map缓存,所以SharedPreference读的效率很高,但是写得时候都是通过FileOutputStreame文件IO得方式完成数据更新操作。


20191224163605643.png

MMKV


利用mmap完成数据的读写,读写高效。
SharedPreferenceMMKV
读写方式IOmmap
数据格式XML总体结构、整型编码、二进制
更新方式全量更新增量与全量写入
SharedPreferences注意点

    只要file name相同,拿到的就是同一个SharedPreferencesImpl对象,内部有缓存机制,首次获取才会创建对象。在SharedPreferencesImpl构造方法中,会开启子线程把对应的文件key-value全部加载进内存,加载结束后,mLoaded被设置为true。调用getXXX方法时,会阻塞等待直到mLoaded为true,也就是getXXX方法是有可能阻塞UI线程的,另外,调用contains和 edit等方法也是。写数据时,会先拿到一个EditorImpl对象,然后putXXX,这时只是把数据写入到内存中,最后调用commit或者apply方法,才会真正写入文件。不管是commit还是apply方法,第一步都是调用commitToMemory方法生成一个MemoryCommitResult对象,注意这里会先处理clear旧的key-value,再处理新添加的key-value,另外value为this或者null都表示需要被remove掉。调用commit方法,就会同步执行写入文件的操作,该方法是耗时操作,不能在主线程中调用,该方法最后会返回成功或失败结果。调用apply方法,就会把任务放到QueuedWork的队列中,然后在HandlerThread中执行,然后apply方法会立即返回。但如果是Android8.0之前,这里就是放到QueuedWork的一个单线程中执行了。最后是写入文件,会先把原有的文件命名为bak备份文件,然后创建新的文件全量写入,写入成功后,把bak备份文件删除掉。
安全


基于Android的沙盒模式,在内存读写的方式上做了改变,所以不存在应用程序之前的安全问题。

MMKV使用ProtoBuf 编码,另外增加了内部实现的加密模式(AES CFB),相比SharedPrefrence,在文件暴露的情况下MMKV的数据不具有可读性。
在TV中的应用


配置参数较多、需要频繁读写修改参数的场景

可以提高读写耗时,减少SP带来的耗时成本和操作不当引发的ANR
源码解读

初始化

public static String initialize(Context context) {        String root = context.getFilesDir().getAbsolutePath() + "/mmkv";        MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;        return initialize(root, (MMKV.LibLoader)null, logLevel);    }    public static String initialize(String rootDir, LibLoader loader, MMKVLogLevel logLevel) {        if (loader != null) {            if (BuildConfig.FLAVOR.equals("SharedCpp")) {                loader.loadLibrary("c++_shared");            }            loader.loadLibrary("mmkv");        } else {            if (BuildConfig.FLAVOR.equals("SharedCpp")) {                System.loadLibrary("c++_shared");            }            System.loadLibrary("mmkv");        }        MMKV.rootDir = rootDir;        jniInitialize(MMKV.rootDir, logLevel2Int(logLevel));        return rootDir;    }
1.当不指定目录的时候,会创建一个app内的/data/data/包名/files/mmkv的目录。所有的文件都保存在里面;

2.加载两个so库,c++_shared以及mmkv, 根据打包配置来选择是否要加载c++_shared
native_bridge.cppMMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir, jint logLevel) {    if (!rootDir) {        return;    }    const char *kstr = env->GetStringUTFChars(rootDir, nullptr);    if (kstr) {        //获取rootDir的url char指针数组字符串,调用MMKV::initializeMMKV进一步初始化。        MMKV::initializeMMKV(kstr, (MMKVLogLevel) logLevel);        env->ReleaseStringUTFChars(rootDir, kstr);    }}MMKV.cppvoid initialize() {    //创建了MMKV实例的散列表    g_instanceDic = new unordered_map<string, MMKV *>;    g_instanceLock = new ThreadLock();    g_instanceLock->initialize();    mmkv::DEFAULT_MMAP_SIZE = mmkv::getPageSize();    MMKVInfo("page size:%d", DEFAULT_MMAP_SIZE);}ThreadOnceToken_t once_control = ThreadOnceUninitialized;void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {    g_currentLogLevel = logLevel;    //初始化全局的线程锁ThreadLock    ThreadLock::ThreadOnce(&once_control, initialize);    g_rootDir = rootDir;    //创建文件夹    mkPath(g_rootDir);    MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());}MMKV 的实例化

java层的实例化


defaultMMKV
public static MMKV defaultMMKV() {        if (rootDir == null) {            throw new IllegalStateException("You should Call MMKV.initialize() first.");        }        long handle = getDefaultMMKV(SINGLE_PROCESS_MODE, null);        return new MMKV(handle);    }//构造函数private MMKV(long handle) {    nativeHandle = handle;}
getDefaultMMKV Native层做好实例化工作返回一个long类型的handle,以这个handler作为Java层MMKV的构造参数

mmkvWithID

与defaultMMKV区别就是多了参数设置
public static MMKV mmkvWithID(String mmapID) {        if (rootDir == null) {            throw new IllegalStateException("You should Call MMKV.initialize() first.");        }        long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, null);        return new MMKV(handle);    }native层实例化


native-bridge.cpp==>getDefaultMMKV

MMKV.cpp==>mmkvWithID 默认的ID为mmkv.default
native-bridge.cppMMKV_JNI jlong getDefaultMMKV(JNIEnv *env, jobject obj, jint mode, jstring cryptKey) {    MMKV *kv = nullptr;    if (cryptKey) {        string crypt = jstring2string(env, cryptKey);        if (crypt.length() > 0) {            kv = MMKV::defaultMMKV((MMKVMode) mode, &crypt);        }    }    if (!kv) {        kv = MMKV::defaultMMKV((MMKVMode) mode, nullptr);    }    return (jlong) kv;}MMKV.cpp#define DEFAULT_MMAP_ID "mmkv.default"MMKV *MMKV::defaultMMKV(MMKVMode mode, string *cryptKey) {#ifndef MMKV_ANDROID    return mmkvWithID(DEFAULT_MMAP_ID, mode, cryptKey);#else    return mmkvWithID(DEFAULT_MMAP_ID, DEFAULT_MMAP_SIZE, mode, cryptKey);#endif}MMKV.hstatic MMKV *mmkvWithID(const std::string &mmapID,                            int size = mmkv::DEFAULT_MMAP_SIZE,                            MMKVMode mode = MMKV_SINGLE_PROCESS,                            std::string *cryptKey = nullptr,                            MMKVPath_t *relativePath = nullptr);
只要是实例化,最后都是调用mmkvWithID进行实例化。默认的mmkv的id就是mmkv.default

mmkvWithID
MMKV.cppMMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {    if (mmapID.empty()) {        return nullptr;    }    SCOPED_LOCK(g_instanceLock);    //取 mmapID relativePath MMKV_PATH_SLASH 的 md5值作为key    auto mmapKey = mmapedKVKey(mmapID, relativePath);    auto itr = g_instanceDic->find(mmapKey);    if (itr != g_instanceDic->end()) {        MMKV *kv = itr->second;        return kv;    }    if (relativePath) {        if (!isFileExist(*relativePath)) {            if (!mkPath(*relativePath)) {                return nullptr;            }        }        MMKVInfo("prepare to load %s (id %s) from relativePath %s", mmapID.c_str(), mmapKey.c_str(),                 relativePath->c_str());    }    //实例化    auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath);    (*g_instanceDic)[mmapKey] = kv;    return kv;}
将所有的MMKV实例都会保存在之前实例化的g_instanceDic散列表中。其中mmkv每一个id对应一个文件的路径:
    相对路径(android中是 data/data/包名/files/mmkv) + / + mmkvID

如果发现对应路径下的mmkv在散列表中已经缓存了,则直接返回。否则就会把相对路径保存下来,传递给MMKV进行实例化,并保存在g_instanceDic散列表中。
MMKV 的构造函数

MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath)    : m_mmapID(mmapedKVKey(mmapID, relativePath)) // historically Android mistakenly use mmapKey as mmapID    , m_path(mappedKVPathWithID(m_mmapID, mode, relativePath))    , m_crcPath(crcPathWithID(m_mmapID, mode, relativePath))    , m_file(new MemoryFile(m_path, size, (mode & MMKV_ASHMEM) ? MMFILE_TYPE_ASHMEM : MMFILE_TYPE_FILE))    , m_metaFile(new MemoryFile(m_crcPath, DEFAULT_MMAP_SIZE, m_file->m_fileType))    , m_metaInfo(new MMKVMetaInfo())    , m_crypter(nullptr)    , m_lock(new ThreadLock())    , m_fileLock(new FileLock(m_metaFile->getFd(), (mode & MMKV_ASHMEM)))    , m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))    , m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))    , m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0 || (mode & CONTEXT_MODE_MULTI_PROCESS) != 0) {    m_actualSize = 0;    m_output = nullptr;    if (cryptKey && cryptKey->length() > 0) {        m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());    }    m_needLoadFromFile = true;    m_hasFullWriteback = false;    m_crcDigest = 0;    m_sharedProcessLock->m_enable = m_isInterProcess;    m_exclusiveProcessLock->m_enable = m_isInterProcess;    // sensitive zone    {        SCOPED_LOCK(m_sharedProcessLock);        loadFromFile();    }}
    1.m_mmapID MMKV的ID通过mmapedKVKey创建:
string mmapedKVKey(const string &mmapID, MMKVPath_t *relativePath) {    if (relativePath && g_rootDir != (*relativePath)) {        return md5(*relativePath + MMKV_PATH_SLASH + string2MMKVPath_t(mmapID));    }    return mmapID;}
mmkvID就是经过md5后对应缓存文件对应的路径。
    2.m_path mmkv 缓存的路径通过mappedKVPathWithID生成
MMKVPath_t mappedKVPathWithID(const string &mmapID, MMKVMode mode, MMKVPath_t *relativePath) {#ifndef MMKV_ANDROID...#else    if (mode & MMKV_ASHMEM) {        return ashmemMMKVPathWithID(encodeFilePath(mmapID));    } else if (relativePath) {#endif        return *relativePath + MMKV_PATH_SLASH + encodeFilePath(mmapID);    }    return g_rootDir + MMKV_PATH_SLASH + encodeFilePath(mmapID);}
能看到这里是根据当前的mode初始化id,如果不是ashmem匿名共享内存模式进行创建,则会和上面的处理类似。id就是经过md5后对应缓存文件对应的路径。

注意这里mode设置的是MMKV_ASHMEM,也就是ashmem匿名共享内存模式则是如下创建方法:
constexpr char ASHMEM_NAME_DEF[] = "/dev/ashmem";MMKVPath_t ashmemMMKVPathWithID(const MMKVPath_t &mmapID) {    return MMKVPath_t(ASHMEM_NAME_DEF) + MMKV_PATH_SLASH + mmapID;}
实际上就是在驱动目录下的一个内存文件地址。
    3.m_crcPath 一个.crc文件的路径。这个crc文件实际上用于保存crc数据校验key,避免出现传输异常的数据进行保存了。4.m_file 一个依据m_path构建的内存文件MemoryFile对象。5.m_metaFile 一个依据m_crcPath构建的内存文件MemoryFile对象。6.m_metaInfo 一个MMKVMetaInfo结构体,这个结构体一般是读写的时候,带上的MMKV的版本信息,映射的内存大小,加密crc的key等。7.m_crypter 默认是一个AESCrypt 对称加密器8.m_lock ThreadLock线程锁9.m_fileLock 一个以m_metaFile的fd 文件锁10.m_sharedProcessLock 类型是InterProcessLock,这是一种文件共享锁11.m_exclusiveProcessLock 类型是InterProcessLock,这是一种排他锁12.m_isInterProcess 判断是否打开了多进程模式的标志位,一旦关闭了,所有进程锁都会失效。
Ashmem匿名共享内存


Anonymous Shared Memory-Ashmem

简单理解:

共享内存是Linux自带的一种IPC机制,Android直接使用了该模型,不过做出了自己的改进,进而形成了Android的匿名共享内存(Anonymous Shared Memory-Ashmem)

应用:

APP进程同SurfaceFlinger共用一块内存,如此,就不需要进行数据拷贝,APP端绘制完毕,通知SurfaceFlinger端合成,再输出到硬件进行显示即可

更多文章

https://www.jianshu.com/p/6a8513fdb792

https://www.jianshu.com/p/d9bc9c668ba6
多进程MMKV实例化


多进程通信的过程
        服务端创建MMKV实例        m_ashmemMMKV = MMKV.mmkvWithAshmemID(BenchMarkBaseService.this, id, AshmemMMKV_Size,MMKV.MULTI_PROCESS_MODE, CryptKey);                Aidl传递实体        ParcelableMMKV(m_ashmemMMKV);                Aidl传递实体 ParcelableMMKV字段        mmapID = mmkv.mmapID();        ashmemFD = mmkv.ashmemFD();        ashmemMetaFD = mmkv.ashmemMetaFD();        cryptKey = mmkv.cryptKey();                客户端获取传递实体ParcelableMMKV        AshmemMMKV ashmemMMKV = IAshmemMMKV.Stub.asInterface(service);        ParcelableMMKV parcelableMMKV = ashmemMMKV.GetAshmemMMKV();                客户端获取真正的操作数据的MMKV实例        parcelableMMKV.toMMKV()        public MMKV toMMKV() {            if (ashmemFD >= 0 && ashmemMetaFD >= 0) {                return MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);            }            return null;        }                看一下mmkvWithAshmemFD        MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);
mmkvWithAshmemFD
MMKV *MMKV::mmkvWithAshmemFD(const string &mmapID, int fd, int metaFD, string *cryptKey) {    if (fd < 0) {        return nullptr;    }    SCOPED_LOCK(g_instanceLock);    auto itr = g_instanceDic->find(mmapID);    if (itr != g_instanceDic->end()) {        MMKV *kv = itr->second;#    ifndef MMKV_DISABLE_CRYPT        kv->checkReSetCryptKey(fd, metaFD, cryptKey);#    endif        return kv;    }    auto kv = new MMKV(mmapID, fd, metaFD, cryptKey);    (*g_instanceDic)[mmapID] = kv;    return kv;}MMKV::MMKV(const string &mmapID, int ashmemFD, int ashmemMetaFD, string *cryptKey)    : m_mmapID(mmapID)    , m_path(mappedKVPathWithID(m_mmapID, MMKV_ASHMEM, nullptr))    , m_crcPath(crcPathWithID(m_mmapID, MMKV_ASHMEM, nullptr))    , m_dic(nullptr)    , m_dicCrypt(nullptr)    , m_file(new MemoryFile(ashmemFD))    , m_metaFile(new MemoryFile(ashmemMetaFD))    , m_metaInfo(new MMKVMetaInfo())    , m_crypter(nullptr)    , m_lock(new ThreadLock())    , m_fileLock(new FileLock(m_metaFile->getFd(), true))    , m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))    , m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))    , m_isInterProcess(true) {encode 写入数据

encodeString

MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {    MMKV *kv = reinterpret_cast<MMKV *>(handle);    if (kv && oKey) {        string key = jstring2string(env, oKey);        if (oValue) {            string value = jstring2string(env, oValue);            return (jboolean) kv->set(value, key);        } else {            kv->removeValueForKey(key);            return (jboolean) true;        }    }    return (jboolean) false;}bool MMKV::set(const string &value, MMKVKey_t key) {    if (isKeyEmpty(key)) {        return false;    }    auto data = MiniPBCoder::encodeDataWithObject(value);    return setDataForKey(std::move(data), key);}
    1.encodeDataWithObject 编码压缩内容2.setDataForKey 保存数据
setDataForKey


保存数据到映射的文件
bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key) {    if (data.length() == 0 || isKeyEmpty(key)) {        return false;    }    SCOPED_LOCK(m_lock);    SCOPED_LOCK(m_exclusiveProcessLock);    checkLoadData();    auto ret = appendDataWithKey(data, key);    if (ret) {        m_dic[key] = std::move(data);        m_hasFullWriteback = false;    }    return ret;}
设置了互斥锁,和线程锁。整个步骤分为两步骤:
    1.checkLoadData 保存数据之前,校验已经存储的数据2.appendDataWithKey 进行数据的保存
appendDataWithKey

bool MMKV::appendDataWithKey(const MMBuffer &data, MMKVKey_t key) {    size_t keyLength = key.length();    // size needed to encode the key    size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength);    // size needed to encode the value    size += data.length() + pbRawVarint32Size((int32_t) data.length());    SCOPED_LOCK(m_exclusiveProcessLock);    bool hasEnoughSize = ensureMemorySize(size);    if (!hasEnoughSize || !isFileValid()) {        return false;    }    m_output->writeString(key);    m_output->writeData(data); // note: write size of data    auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;    if (m_crypter) {        m_crypter->encrypt(ptr, ptr, size);    }    m_actualSize += size;    updateCRCDigest(ptr, size);    return true;}
判断是否有足够的空间,没有则调用ensureMemorySize进行扩容,实在无法从内存中映射出来,那说明系统没空间了就返回异常。

正常情况下,是往全局缓冲区CodedOutputData 先后在文件内存的末尾写入key和value的数据。并对这部分的数据进行一次加密,最后更新这个存储区域的crc校验码。

这里实际上是调用了CodedOutputData的writeString把数据保存到映射的内存中。
void CodedOutputData::writeString(const string &value) {    size_t numberOfBytes = value.size();    this->writeRawVarint32((int32_t) numberOfBytes);    if (m_position + numberOfBytes > m_size) {        auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) +                   ", m_size: " + to_string(m_size);        throw out_of_range(msg);    }    memcpy(m_ptr + m_position, ((uint8_t *) value.data()), numberOfBytes);    m_position += numberOfBytes;}decode MMKV读取数据


MMKV读取数据
MMKV_JNI jstring decodeString(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jstring oDefaultValue) {    MMKV *kv = reinterpret_cast<MMKV *>(handle);    if (kv && oKey) {        string key = jstring2string(env, oKey);        string value;        bool hasValue = kv->getString(key, value);        if (hasValue) {            return string2jstring(env, value);        }    }    return oDefaultValue;}bool MMKV::getString(MMKVKey_t key, string &result) {    if (isKeyEmpty(key)) {        return false;    }    SCOPED_LOCK(m_lock);    auto &data = getDataForKey(key);    if (data.length() > 0) {        try {            result = MiniPBCoder::decodeString(data);            return true;        } catch (std::exception &exception) {            MMKVError("%s", exception.what());        }    }    return false;}
大致可以分分为两步:
    1.getDataForKey 通过key找缓存的数据2.decodeString 对获取到的数据进行解码
getDataForKey

const MMBuffer &MMKV::getDataForKey(MMKVKey_t key) {    checkLoadData();    auto itr = m_dic.find(key);    if (itr != m_dic.end()) {        return itr->second;    }    static MMBuffer nan;    return nan;}
由于是一个多进程的组件,因此每一次进行读写之前都需要进行一次checkLoadData的校验。而这个方法从上文可知,通过crc校验码,写回计数,文件长度来判断文件是否发生了变更,是否追加删除数据,从而是否需要重新充内存文件中获取数据缓存到m_dic。

也因此,在getDataForKey方法中,可以直接从m_dic中通过key找value。
decodeString

string MiniPBCoder::decodeString(const MMBuffer &oData) {    MiniPBCoder oCoder(&oData);    return oCoder.decodeOneString();}string MiniPBCoder::decodeOneString() {    return m_inputData->readString();}string CodedInputData::readString() {    int32_t size = readRawVarint32();    if (size < 0) {        throw length_error("InvalidProtocolBuffer negativeSize");    }    auto s_size = static_cast<size_t>(size);    if (s_size <= m_size - m_position) {        string result((char *) (m_ptr + m_position), s_size);        m_position += s_size;        return result;    } else {        throw out_of_range("InvalidProtocolBuffer truncatedMessage");    }}
能看到实际上很简单就是从m_dic找到对应的MMBuffer数据,此时的可以通过CodedInputData对MMBuffer对应的内存块(已经知道内存起始地址,长度)进行解析数据。
总结


img

MMKV读写是直接读写到mmap文件映射的内存上,绕开了普通读写io需要进入内核,写到磁盘的过程。光是这种级别优化,都可以拉开三个数量级的性能差距。但是也诞生了一个很大的问题,一个进程在32位的机子中有4g的虚拟内存限制,而我们把文件映射到虚拟内存中,如果文件过大虚拟内存就会出现大量的消耗最后出现异常,对于不熟悉Linux的朋友就无法理解这种现象。

有几个关于MMKV使用的注意事项:
    1.保证每一个文件存储的数据都比较小,也就说需要把数据根据业务线存储分散。这要就不会把虚拟内存消耗过快。2.还需要在适当的时候释放一部分内存数据,比如在App中监听onTrimMemory方法,在Java内存吃紧的情况下进行MMKV的trim操作(不准确,我们暂时以此为信号,最好自己监听进程中内存使用情况)。2.在不需要使用的时候,最好把MMKV给close掉。甚至调用exit方法。

参考文章:https://www.jianshu.com/p/c12290a9a3f7

官方Demo:https://github.com/Tencent/MMKV/tree/master/Android/MMKV

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 19:46 , Processed in 0.093970 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表