从零手写实现简易版MMKV(一)

本文从零开始介绍如何手写简易版MMKV(EZKV),涵盖了核心功能的实现,包括SDK初始化、EZKV实例初始化、数据写入与读取。讲解了内存映射文件、protobuf变长编码等关键技术,并提供了代码实现的详细步骤。文章最后总结了已完成的功能,并预告了后续将添加的数据校验和多线程支持。
摘要由CSDN通过智能技术生成

概述

MMKV是支持多平台的高性能键值对持久化存储组件,其核心原理是利用mmap内存映射文件,关于它的详细介绍和更多原理参看MMKV开源git地址

从零开始手写(其实是抄写-.-!)简易版MMKV(另起名叫EZKV),即先只关注最核心的功能(实现最小化系统),再从主干开枝散叶,逐步进行完善。

预期目标

第一版目标仅实现最基本的key-value数据存储和读取功能,先忽略数据加密、数据校验、数据容错、跨平台、线程同步、进程同步、ASHMEM等等等。

1-EZKV接口

其中还涉及到几个比较关键的前置知识,需要提前熟悉:

技术设计

1-模块分层
主要逻辑在Native层,Java作为门面通过JNI调用C/C++。

以下列举了几个关键类的作用:
1-类ezkv
EZKV即操作门面,一个EZKV实例对应一个存储key-value数据的文件:

  • m_mmapID:EZKV ID,默认为"ezkv.default"
  • m_path:数据文件路径,EZKVPath_t是std::string的类型别名
  • m_dic:缓存key-value的容器,EZKVMAP即std::unordered_map<std::string, KeyValueHolder>
  • m_file:表示内存映射文件
  • m_actualSize:记录存储的key-value数据的内容的大小
  • m_output:用于写入key-value的辅助类

1-类memoryfile
MemoryFile表示内存映射文件,封装内存映射相关操作,记录内存信息:

  • m_name:数据文件路径
  • m_fd:数据文件描述符
  • m_ptr:m_fd内存映射分配的起始地址
  • m_size:文件大小,也是映射内存分配的大小

1-结构体keyvalueholder
KeyValueHolder用于记录一条KV对。注意m_dic中的value项不是直接存的value值,而是KeyValueHolder,通过其中记录的信息再来获取映射内存中的值:

  • computedKVSize:基于offset的偏移量 = key size + key + value size, 可以直接定位到该KV对的value值
  • keySize:key值占用的大小
  • valueSize:value值占用的大小
  • offset:从m_ptr开始到该KV对的偏移量,可以快速定位到该KV对

MMBuffer作用相当于读取或写入时的缓冲区:

union {
   
  struct {
   
    MMBufferCopyFlag isNoCopy;
    size_t size; // 几字节数据
    void *ptr; // 内存起始地址
  };
  struct {
   
    uint8_t paddedSize;
    // make at least 10 bytes to hold all primitive types (negative int32, int64, double etc) on 32 bit device
    // on 64 bit device it's guaranteed larger than 10 bytes
    uint8_t paddedBuffer[10]; // 用于存储较小的基本类型的值,存储在栈上,而不是堆上,避免malloc和free
  };
};

1-类codedoutputdata
CodedOutputData用于写入数据,封装了写入的方法:

  • m_ptr:写入到的区域的起始地址
  • m_size:可写入区域的大小
  • m_position:当前写入位置

1-codedinputdata
CodedOutputData用于读取数据,封装了读取的方法:

  • m_ptr:从区域读取的起始地址
  • m_size:可读取区域的大小
  • m_position:当前读取位置

编码实现

完整项目工程见GitHub:https://github.com/chidehang/EZKV/tree/practice01

SDK初始化

初始化主要完成三件事:
1.加载库、动态注册函数
2.初始化一些全局变量
3.创建存放数据文件的目录

提供给接入方的初始化方法,首先获取存储数据文件的目录的路径,接着加载库,最后调用native方法进行初始化,核心逻辑都在native方法中:

[EZKV.java]

public static String initialize(Context context) {
   
  String root = context.getFilesDir().getAbsolutePath() + "/ezkv";

  // 加载so库
  System.loadLibrary("ezkv");

  // 传入目录进行初始化
  nInitialize(root);
  EZKV.rootDir = root;
  return EZKV.rootDir;
}

private static native void nInitialize(String rootDir);

JNI中函数有静态注册和动态注册,这里采用动态注册的方式,当执行loadLibrary后会回调JNI_OnLoad函数,在该函数中进行函数绑定注册:

[native-bridge.cpp]

extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    // 提前缓存后续会用到但不会改变的jclass和g_fileID
    g_currentJVM = vm;
    JNIEnv *env;
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    if (g_cls) {
        env->DeleteGlobalRef(g_cls);
    }
    static const char *clsName = "com/cdh/ezkv/EZKV";
    jclass instance = env->FindClass(clsName);
    if (!instance) {
        LOGE("fail to locate class: %s", clsName);
        return -2;
    }
    g_cls = reinterpret_cast<jclass>(env->NewGlobalRef(instance));
    if (!g_cls) {
        LOGE("fail to create global reference for %s", clsName);
        return -3;
    }

    // 动态注册函数
    int ret = registerNativeMethods(env, g_cls);
    if (ret != 0) {
        LOGE("fail to register native methods for class %s, ret = %d", clsName, ret);
        return -4;
    }

    g_fileID = env->GetFieldID(g_cls, "nativeHandle", "J");
    if (!g_fileID) {
        LOGE("fail to locate fileID");
        return -5;
    }

    return JNI_VERSION_1_6;
}

TIPS:通常在库加载时提前缓存会用到的jclass和g_fileID,避免JNI调用时再查找,提升效率。

进一步看registerNativeMethods函数,在该方法中注册函数映射表:

[native-bridge.cpp]

static JNINativeMethod g_methods[] = {
        {"nInitialize", "(Ljava/lang/String;)V", (void *) ezkv::nInitialize},
        {"nGetDefaultEZKV", "()J", (void *) ezkv::nGetDefaultEZKV},
        {"nPageSize", "()J", (void *) ezkv::nPageSize},
        {"nEncodeInt", "(JLjava/lang/String;I)Z", (void *) ezkv::nEncodeInt},
        {"nDecodeInt", "(JLjava/lang/String;I)I", (void *) ezkv::nDecodeInt},
        {"nEncodeString", "(JLjava/lang/String;Ljava/lang/String;)Z", (void *) ezkv::nEncodeString},
        {"nDecodeString", "(JLjava/lang/String;Ljava/lang/String;)Ljava/lang
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值