MMKV源码详解

文章目录

  • 前言
  • 一、MMKV简介
    • 1.mmap
    • 2.protobuf
  • 二、MMKV 源码详解
    • 1.MMKV初始化
    • 2.MMKV对象获取
    • 3.文件摘要的映射
    • 4.loadFromFile 从文件加载数据
    • 5.encode 数据写入
  • 总结


前言

谈到轻量级的数据持久化,在 Android 开发过程中,大家首先想到的应该就是 SharedPreferences(以下简称 SP),其存储数据是以 key-value 键值对的形式保存在 data/data/<package name>/shared_prefs 路径下的 xml 文件中,使用 I/O 流 进行文件的读写。通常用来保存应用中的一些简单的配置信息,如用户名、密码、自定义参数的设置等。

需要注意的是:SP 中的 value 值只能是 intbooleanfloatlongStringStringSet 这些类型的数据。

作为 Android 原生库中自带的轻量级存储类,SP 在使用方式上还是很便捷的,但是也存在以下的一些问题:

  1. 通过 getSharedPreferences() 方法获取 SP 实例对象,从首次初始化到读到数据会存在延迟,因为读文件操作需阻塞调用的线程直到文件读取完毕,因此不要在主线程调用,可能会对 UI 界面的流畅度造成影响。(线程阻塞)
  2. SP 在跨进程共享方面无法保证线程安全,因此在 Android 7.0 之后便不再对跨进程模式进行支持。(跨进程共享)
  3. 将数据写入到 xml 文件需要经过两次数据拷贝,如果数据量过大,将会有很大的性能损耗,效率不高。(两次拷贝)

为了解决上述问题,腾讯的微信团队基于 MMAP 研发了 MMKV 来代替 SP


一、MMKV简介

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。支持通过 AES 算法对 protobuf 文件进行加密,并且引入 循环冗余校验码(CRC) 对文件的完整性进行校验。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Windows 平台,并且开源。

1.mmap

mmapmemory map 的缩写,也就是内存映射地址映射,是 Linux 操作系统中的一种系统调用,它的作用是将一个文件或者其它对象映射到进程的地址空间,实现磁盘地址和进程虚拟地址空间一段虚拟地址的一一对应关系。通过 mmap 这个系统调用我们可以让进程之间通过映射到同一个普通文件实现共享内存,普通文件被映射到进程地址空间当中后,进程可以像访问普通内存一样对文件进行一系列操作,而不需要通过 I/O 系统调用来读取或写入。

mmap 函数 声明如下:

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

函数各个参数的含义如下:

  • addr:待映射的虚拟内存区域在进程虚拟内存空间中的起始地址(虚拟内存地址),通常设置成 NULL,意思就是完全交由内核来帮我们决定虚拟映射区的起始地址(要按照 PAGE_SIZE(4K) 对齐)。
  • length:待申请映射的内存区域的大小,如果是匿名映射,则是要映射的匿名物理内存有多大,如果是文件映射,则是要映射的文件区域有多大(要按照 PAGE_SIZE(4K) 对齐)。
  • prot:映射区域的保护模式。有 PROT_READPROT_WRITEPROT_EXEC等。
  • flags标志位,可以控制映射区域的特性。常见的有 MAP_SHAREDMAP_PRIVATE 等。
  • fd文件描述符,用于指定映射的文件 (由 open( ) 函数返回)。
  • offset:映射的起始位置,表示被映射对象 (即文件) 从那里开始对映,通常设置为 0,该值应该为大小为PAGE_SIZE(4K)的整数倍。

mmap 函数会将一个文件或其他对象映射到进程的地址空间中,并返回一个指向映射区域的指针,进程可以使用指针来访问映射区域的数据,就像访问内存一样。关于 mmap 的映射原理及源码分析,有兴趣的同学可看一下这篇文章。

2.protobuf

Protocol Buffers 简称:Protobuf,是 Google 提供的一个具有高效的协议数据交换格式工具库,用于高效地序列化和反序列化结构化数据,通常用于网络通信数据存储等场景。

ProtobufXmlJson 序列化的方式不同,采用了二进制字节的序列化方式,用字段索引字段类型通过算法计算得到字段之间的关系映射,从而达到更高的时间效率和空间效率,特别适合对数据大小和传输速率比较敏感的场合使用。

Protobuf 采用了一种 TLV (Tag-Length-Value) 的格式进行编码,其格式如下:
在这里插入图片描述
由图可知,每条字段都由 TagLengthValue 三部分组成,其中 Length 是可选的。Tag 字段又由 field_numberwire_type 两部分组成,其中:

  • field_numbermessage 定义字段时指定的字段编号;
  • wire_typeProtobuf 编码类型,由三位 bit 构成,故能表示 8 种类型的编码方案,目前已定义 6 种,其中两种已被废弃。

并且 Tag 采用了 Varints 编码,这是一种可变长的 int 编码(类似 dex 文件的 LEB128),其编码规则如下:

  • 第一位标明了是否需要读取下一字节;
  • 存储了数值的补码,且低位在前高位在后。

Protobuf 的主要优点包括:

  • 高效性Protobuf 序列化后的二进制数据通常比其他序列化格式(比如常用的 JSON)更小,并且序列化和反序列化的速度更快,这对于性能敏感的应用非常有益;
  • 简洁性Protobuf 使用一种定义消息格式的语法,它允许定义字段类型、顺序和规则(消息结构更加清晰和简洁);
  • 版本兼容性Protobuf 支持向前和向后兼容的版本控制,使得在消息格式发生变化时可以更容易地处理不同版本的通信;
  • 语言无关性Protobuf 定义的消息格式可以在多种编程语言中使用,这有助于跨语言的通信和数据交换(截至本文发布目前官方支持的有C++/C#/Dart/Go/Java/Kotlin/python);
  • 自动生成代码Protobuf 通常与相应的工具一起使用,可以自动生成代码,包括序列化/反序列化代码和相关的类(减少了手动编写代码的工作量,提高效率)。

MMKV 中通过 MiniPBCoder 完成了 Protobuf 的序列化及反序列化。可以通过 MiniPBCoder::decodeMapMMKV 存储的 Protobuf 文件反序列化为对应的 Map,也可以通过 MiniPBCoder::encodeDataWithObjectMap 序列化为对应存储的字节流。

二、MMKV 源码详解

1.MMKV初始化

通过 MMKV.initialize 方法可以实现 MMKV 的初始化:

public class MMKV implements SharedPreferences, SharedPreferences.Editor {// call on program start 程序启动时调用public static String initialize(Context context) {// 使用内部存储空间下的 mmkv 文件夹作为根目录String root = context.getFilesDir().getAbsolutePath() + "/mmkv";// 继续调用 initialize 方法传入根目录 root 进行初始化return initialize(root, null);}// 记录 mmkv 存储使用的根目录static private String rootDir = null; public static String initialize(String rootDir, LibLoader loader) {...... // 省略MMKV.rootDir = rootDir; // 保存根目录// Native 层初始化jniInitialize(MMKV.rootDir);return rootDir;}// JNI 调用到 Native 层继续初始化private static native void jniInitialize(String rootDir);
}

MMKV 的初始化,主要是将存储根目录通过 jniInitialize 传入到 Native 层,接下来看看 Native 层的初始化操作:

// native-bridge.cpp
namespace mmkv { // mmkv 命名空间MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir) {if (!rootDir) { // 如果根目录为空则直接返回return;}// 将 jstring 类型转化为 c 中的 const char * 类型const char *kstr = env->GetStringUTFChars(rootDir, nullptr);if (kstr) {// 调用 MMKV::initializeMMKV 对 MMKV 进行初始化MMKV::initializeMMKV(kstr);// c 和 c++ 与 Java 不同,用完需主动释放掉env->ReleaseStringUTFChars(rootDir, kstr);}
}
}// MMKV.cpp
static unordered_map<std::string, MMKV *> *g_instanceDic;
static ThreadLock g_instanceLock;
static std::string g_rootDir;void initialize() {// 获取一个 unordered_map, 类似于 Java 中的 HashMapg_instanceDic = new unordered_map<std::string, MMKV *>;// 初始化线程锁g_instanceLock = ThreadLock();......
}void MMKV::initializeMMKV(const std::string &rootDir) {// 由 Linux Thread 互斥锁和条件变量保证 initialize 函数在一个进程内只会执行一次static pthread_once_t once_control = PTHREAD_ONCE_INIT;// 进行初始化操作pthread_once(&once_control, initialize);// 将根目录保存到全局变量g_rootDir = rootDir;// 字符串拷贝库函数,这里是防止根目录被修改字符串的内容,因此拷贝副本使用char *path = strdup(g_rootDir.c_str());if (path) {// 根据路径, 生成目标地址的目录mkPath(path);free(path); // 释放内存}
}

可以看到 initializeMMKV 的主要任务是初始化数据,以及创建根目录:

  • 创建 MMKV 对象的缓存散列表 g_instanceDic
  • 创建一个线程锁 g_instanceLock
  • mkPath 根据字符串创建文件目录。

pthread_once_t: 类似于 Java 的单例,其 initialize 方法在进程内只会执行一次。

// MmapedFile.cpp
bool mkPath(char *path) {// 定义 stat 结构体用于描述文件的属性struct stat sb = {};bool done = false;// 指向字符串起始地址char *slash = path;while (!done) {// 移动到第一个非 "/" 的下标处slash += strspn(slash, "/");// 移动到第一个 "/" 下标出处slash += strcspn(slash, "/");done = (*slash == '\0');*slash = '\0';if (stat(path, &sb) != 0) {// 执行创建文件夹的操作, C 中无 mkdirs 的操作, 需要一个一个文件夹的创建if (errno != ENOENT || mkdir(path, 0777) != 0) {MMKVWarning("%s : %s", path, strerror(errno));return false;}}// 若非文件夹, 则说明为非法路径else if (!S_ISDIR(sb.st_mode)) {MMKVWarning("%s: %s", path, strerror(ENOTDIR));return false;}*slash = '/';}return true;
}

mkPath 根据字符串创建好文件目录之后,Native 层的初始化操作便结束了,接下来看看 MMKV 实例构建的过程。

2.MMKV对象获取

通过 mmkvWithID 方法可以获取 MMKV 对象,传入的 mmapID 就对应了 SharedPreferences 中的 name,代表了一个文件对应的 name,而 relativePath 则对应了一个相对根目录的相对路径:

public class MMKV implements SharedPreferences, SharedPreferences.Editor {@Nullablepublic static MMKV mmkvWithID(String mmapID, int mode, String cryptKey, String relativePath) {if (rootDir == null) { throw new IllegalStateException("You should Call MMKV.initialize() first."); } // Native 层 getMMKVWithID 方法,执行完 Native 层初始化, 返回句柄值long handle = getMMKVWithID(mmapID, mode, cryptKey, relativePath);if (handle == 0) {return null;}// 构建一个 Java 的壳对象return new MMKV(handle);}private native static longgetMMKVWithID(String mmapID, int mode, String cryptKey, String relativePath);// jniprivate long nativeHandle; // Java 层持有 Native 层对象的地址从而与 Native 对象通信private MMKV(long handle) {nativeHandle = handle; // 并不是真正的 new 出 Java 层的一个实例对象}
}

调用到 Native 层的 getMMKVWithId 方法,并获取到了一个 handle 构造了 Java 层的 MMKV 对象返回,Java 层通过持有 Native 层对象的地址从而与 Native 对象通信。

// native-bridge.cpp
namespace mmkv {
MMKV_JNI jlong getMMKVWithID(JNIEnv *env, jobject, jstring mmapID, jint mode, jstring cryptKey, jstring relativePath) {MMKV *kv = nullptr;if (!mmapID) { // mmapID 为 null 返回空指针 return (jlong) kv;}// 获取独立存储 mmapIDstring str = jstring2string(env, mmapID); // jstring类型的值转化为c++中的string类型  bool done = false;if (cryptKey) { // 如果需要进行加密,获取用于加密的 key,最后调用 MMKV::mmkvWithID// 获取秘钥string crypt = jstring2string(env, cryptKey);if (crypt.length() > 0) {if (relativePath) {// 获取相对路径string path = jstring2string(env, relativePath);// 通过 mmkvWithID 函数获取一个 MMKV 的对象kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, &path);} else {kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, nullptr);}done = true;}}// 如果不需要加密,则调用 mmkvWithID 不传入加密 key,表示不进行加密 if (!done) { if (relativePath) { string path = jstring2string(env, relativePath); kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, &path); } else { kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, nullptr); } } // 强转成句柄, 返回到 Javareturn (jlong) kv;
}
}

其内部继续调用了 MMKV::mmkvWithID 方法,根据是否传入用于加密的 key 以及是否使用相对路径调用了不同的方法来获取到 MMKV 的对象。

// MMKV.cpp
MMKV *MMKV::mmkvWithID(const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {if (mmapID.empty()) {  // mmapID 为 null 返回空指针 return nullptr;}SCOPEDLOCK(g_instanceLock); // 加锁 // 通过 mmapID 和 relativePath, 组成最终的 mmap 文件路径的 mmapKeyauto mmapKey = mmapedKVKey(mmapID, relativePath);// 通过 mmapKey 在全局缓存中查找 map 中对应的 MMKV 对象并返回auto itr = g_instanceDic->find(mmapKey);if (itr != g_instanceDic->end()) {MMKV *kv = itr->second;return kv;}// 如果找不到,构建路径后构建 MMKV 对象并加入 mapif (relativePath) {// 根据 mappedKVPathWithID 获取 mmap 的最终文件路径// mmapID 使用 md5 加密auto filePath = mappedKVPathWithID(mmapID, mode, relativePath);if (!isFileExist(filePath)) { // 不存在则创建一个文件if (!createFile(filePath)) {return nullptr; // 创建不成功则返回空指针}}...}// 创建实例对象auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath);// 缓存这个 mmapKey(*g_instanceDic)[mmapKey] = kv;return kv;
}

MMKV::mmkvWithID 方法的执行流程如下:

  • 通过 mmapedKVKey 方法对 mmapIDrelativePath 进行结合生成对应的 mmapKey,它会将它们两者的结合经过 md5 加密从而生成对应的 key,主要目的是为了支持不同相对路径下的同名 mmapID
  • 通过 mmapKeyg_instanceDic 这个 map 中查找对应的 MMKV 对象,如果找到直接返回;
  • 如果找不到对应的 MMKV 对象,在构建路径后,构建一个新的 MMKV 对象,加入 map 后返回。

接下来重点关注 MMKV 的构造函数:

// MMKV.cpp
MMKV::MMKV(const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath): m_mmapID(mmapedKVKey(mmapID, relativePath)) // 通过 mmapID 和 relativePath 组成最终的 mmap 文件路径的 mmapKey 赋值给 m_mmapID// 拼装文件的路径, m_path(mappedKVPathWithID(m_mmapID, mode, relativePath))// 拼装 .crc 文件路径, m_crcPath(crcPathWithID(m_mmapID, mode, relativePath))// 将文件摘要信息映射到内存, 4 kb 大小, m_metaFile(m_crcPath, DEFAULT_MMAP_SIZE, (mode & MMKV_ASHMEM) ? MMAP_ASHMEM : MMAP_FILE)......, m_sharedProcessLock(&m_fileLock, SharedLockType)......, m_isAshmem((mode & MMKV_ASHMEM) != 0) {......// 判断是否为 Ashmem 跨进程匿名共享内存if (m_isAshmem) {// 创共享内存的文件m_ashmemFile = new MmapedFile(m_mmapID, static_cast<size_t>(size), MMAP_ASHMEM);m_fd = m_ashmemFile->getFd();} else {m_ashmemFile = nullptr;}// 根据 cryptKey 创建 AES 加解密的引擎 AESCryptif (cryptKey && cryptKey->length() > 0) {m_crypter = new AESCrypt((const unsigned char *) cryptKey->data(), cryptKey->length());}......// sensitive zone{	// 加锁后调用 loadFromFile 根据 m_mmapID 来加载文件中的数据SCOPEDLOCK(m_sharedProcessLock);loadFromFile();}
}

MMKV 构造函数:

  • 进行了一些赋值操作,之后如果需要加密则根据用于加密的 cryptKey 生成对应的 AESCrypt 对象用于 AES 加密;
  • 加锁后通过 loadFromFile 方法根据 m_mmapID 从文件中读取数据,这里的锁是一个跨进程的文件共享锁

MMKV 的构造函数可以看出,MMKV 是支持 Ashmem 共享内存的,当我们不想将文件写入磁盘,但是又想进行跨进程通信,就可以使用 MMKV 提供的 MMAP_ASHMEM

3.文件摘要的映射

// MmapedFile.cpp
MmapedFile::MmapedFile(const std::string &path, size_t size, bool fileType): m_name(path), m_fd(-1), m_segmentPtr(nullptr), m_segmentSize(0), m_fileType(fileType) {if (m_fileType == MMAP_FILE) { // 用于内存映射的文件// open 方法打开文件m_fd = open(m_name.c_str(), O_RDWR | O_CREAT, S_IRWXU);if (m_fd < 0) {MMKVError("fail to open:%s, %s", m_name.c_str(), strerror(errno));} else {FileLock fileLock(m_fd); // 创建文件锁InterProcessLock lock(&fileLock, ExclusiveLockType);SCOPEDLOCK(lock);struct stat st = {}; // 获取文件的信息if (fstat(m_fd, &st) != -1) {m_segmentSize = static_cast<size_t>(st.st_size); // 获取文件大小}// 验证文件的大小是否小于一个内存页, 一般为 4kbif (m_segmentSize < DEFAULT_MMAP_SIZE) {m_segmentSize = static_cast<size_t>(DEFAULT_MMAP_SIZE);// 通过 ftruncate 将文件大小对其到内存页// 通过 zeroFillFile 将文件对其后的空白部分用 0 填充if (ftruncate(m_fd, m_segmentSize) != 0 || !zeroFillFile(m_fd, 0, m_segmentSize)) {close(m_fd);m_fd = -1;removeFile(m_name); // 文件拓展失败, 关闭并移除这个文件return;}}// 通过 mmap 函数将文件映射到内存, 获取内存首地址m_segmentPtr =(char *) mmap(nullptr, m_segmentSize, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);if (m_segmentPtr == MAP_FAILED) {MMKVError("fail to mmap [%s], %s", m_name.c_str(), strerror(errno));close(m_fd);m_fd = -1;m_segmentPtr = nullptr;}}}// 用于共享内存的文件else {......}
}

MmapedFile 构造函数:

  • open 方法打开指定的文件;
  • fileLock 方法创建文件锁;
  • 修正文件大小,最小为 4kb,前 4kb 用于统计数据总大小;
  • mmap 函数将文件映射到内存。

通过 MmapedFile 的构造函数, 我们便能够获取到映射后的内存首地址,操作这块内存时 Linux 内核会负责将内存中的数据同步到文件中。 即使进程意外死亡,也能够通过 Linux 内核的保护机制,将映射后文件的内存数据刷入到文件中,提升了数据写入的可靠性。

4.loadFromFile 从文件加载数据

// MMKV.cpp
void MMKV::loadFromFile() {......// 忽略匿名共享内存相关代码// 若已经进行了文件映射if (m_metaFile.isFileValid()) {m_metaInfo.read(m_metaFile.getMemory()); // 则获取相关数据}// 打开对应的文件,获取文件描述符m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);if (m_fd < 0) {MMKVError("fail to open:%s, %s", m_path.c_str(), strerror(errno));} else {m_size = 0; // 获取文件大小struct stat st = {0};if (fstat(m_fd, &st) != -1) {m_size = static_cast<size_t>(st.st_size);}// 将文件大小对齐到内存页大小的整数倍,用 0 填充不足的部分 if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {......}// 通过 mmap 将文件映射到内存,获取映射后的内存地址m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);if (m_ptr == MAP_FAILED) {......} else {// 读取内存文件的前 32 位, 获取存储数据的真实大小memcpy(&m_actualSize, m_ptr, Fixed32Size);......bool loadFromFile = false, needFullWriteback = false;if (m_actualSize > 0) {// 验证文件的长度if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {// 对文件进行 CRC 校验,如果失败根据策略进行不同对处理 if (checkFileCRCValid()) {loadFromFile = true;} else {// CRC 校验失败,则回调 CRC 异常auto strategic = mmkv::onMMKVCRCCheckFail(m_mmapID);if (strategic == OnErrorRecover) {loadFromFile = true;needFullWriteback = true;}}} else {// 文件大小有误,回调文件长度异常auto strategic = mmkv::onMMKVFileLengthError(m_mmapID);if (strategic == OnErrorRecover) {writeAcutalSize(m_size - Fixed32Size);loadFromFile = true;needFullWriteback = true;}}}// 需要从文件获取数据if (loadFromFile) {......// 构建输入缓存 MMBufferMMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);if (m_crypter) {// 如果需要解密,对文件进行解密 decryptBuffer(*m_crypter, inputBuffer);}// 通过 MiniPBCoder 将 MMBuffer 转换为 Mapm_dic.clear();MiniPBCoder::decodeMap(m_dic, inputBuffer);// 构建输出数据的 CodedOutputDatam_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize,m_size - Fixed32Size - m_actualSize);// 进行重整回写, 剔除重复的数据if (needFullWriteback) {fullWriteback();}} // 说明文件中没有数据, 或者校验失败了else {SCOPEDLOCK(m_exclusiveProcessLock);if (m_actualSize > 0) { // 清空文件中的数据writeAcutalSize(0);}m_output = new CodedOutputData(m_ptr + Fixed32Size, m_size - Fixed32Size);// 重新计算 CRCrecaculateCRCDigest();}......}}......m_needLoadFromFile = false;
}

loadFromFile 函数的执行流程如下:

  • 打开文件并获取文件大小,将文件的大小对齐到内存页的整数倍,不足则补 0(与内存映射的原理有关,内存映射是基于页的换入换出机制实现的);
  • 通过 mmap 函数将文件映射到内存中,得到指向该内存区域的指针 m_ptr
  • 对文件进行长度校验及 CRC 校验(循环冗余校验,可以校验文件完整性),在失败的情况下会根据当前策略进行抉择,如果策略是失败时恢复,则继续读取,并且在最后将 map 中的内容回写到文件;
  • 由指向内存区域的指针 m_ptr 构造出一块用于管理 MMKV 映射内存的 MMBuffer 对象,如果需要解密,通过之前构造的 AESCrypt 对象进行解密;
  • 由于 MMKV 使用了 Protobuf 进行序列化,通过 MiniPBCoder::decodeMap 方法将 Protobuf 转换成对应的 map
  • 构造用于输出的 CodedOutputData 类实例,如果需要回写 (CRC 校验或文件长度校验失败),则调用 fullWriteback 方法将 map 中的数据回写到文件。

5.encode 数据写入

Java 层的 MMKV 类继承了 SharedPreferencesSharedPreferences.Editor 接口并实现了一系列如 putIntputLongputString 等方法,同时也有 encode 的很多重载方法,用于对存储的数据进行修改,下面以 putString 为例:

public class MMKV implements SharedPreferences, SharedPreferences.Editor {...// 省略部分代码public boolean encode(String key, String value) {// 调用 Native 层的 encodeString 方法对数据进行写入操作return this.encodeString(this.nativeHandle, key, value);}@Override public SharedPreferences.Editor putString(String key, @Nullable String value) {// 调用 Native 层的 encodeString 方法对数据进行写入操作this.encodeString(this.nativeHandle, key, value);return this;}private native boolean encodeString(long handle, String key, String value);
}

putString 方法和 encode 方法,都是调用 Native 层的 encodeString 方法对字符串数据进行写入操作。

// native-bridge.cpp
namespace mmkv {MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {// 将 Java 层持有的 NativeHandle 转为对应的 MMKV 对象MMKV *kv = reinterpret_cast<MMKV *>(handle);if (kv && oKey) {string key = jstring2string(env, oKey);if (oValue) { // 若 value 非 NULL// 通过 setStringForKey 函数,将数据存入string value = jstring2string(env, oValue);return (jboolean) kv->setStringForKey(value, key);} else { // 若是 value 为 NULL, 则移除 key 对应的 value 值kv->removeValueForKey(key);return (jboolean) true;}}return (jboolean) false;
}
}

如果 value 值非 NULL,通过 setStringForKey 函数将数据写入,为 NULL 则通过 removeValueForKey 函数移除 key 对应的 value 值。

// MMKV.cpp
bool MMKV::setStringForKey(const std::string &value, const std::string &key) {if (key.empty()) {return false;}// 将数据编码成 ProtocolBufferauto data = MiniPBCoder::encodeDataWithObject(value);// 更新键值对return setDataForKey(std::move(data), key);
}

总结

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/848425.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

题号:BC19 题目:反向输出一个四位数

题号&#xff1a;BC19 题目&#xff1a;反向输出一个四位数 废话不多说&#xff0c;上题目&#xff1a; 解题思路&#xff1a; 我们发现可以用%和/两个操作符就可以解决。 代码如下: int main() {int a 0;scanf("%d ",& a);while (a){printf("%d "…

【Vue】普通组件的注册使用-全局注册

文章目录 一、使用步骤二、练习 一、使用步骤 步骤 创建.vue组件&#xff08;三个组成部分&#xff09;main.js中进行全局注册 使用方式 当成HTML标签直接使用 <组件名></组件名> 注意 组件名规范 —> 大驼峰命名法&#xff0c; 如 HmHeader 技巧&#xf…

华安保险:核心系统分布式升级,提升保费规模处理能力2-3倍 | OceanBase企业案例

在3月20日的2024 OceanBase数据库城市行的活动中&#xff0c;安保险信息科技部总经理王在平发表了以“保险行业核心业务系统分布式架构实践”为主题的演讲。本文为该演讲的精彩回顾。 早在2019年&#xff0c;华安保险便开始与OceanBase接触&#xff0c;并着手进行数据库的升级…

雅欣控制HALL IC 产品选型手册,选择您的专属霍尔芯片(霍尔产品主要包括远翔FD,FS全系列,MST全系列霍尔)

HALLICs 应用领域 Applications 应用案例 雅欣为各个应用场景匹配专属HALL元器件 合作伙伴 Partners

专属编程笔记

Utils目录作用 在软件开发中&#xff0c;Utils&#xff08;或 Utilities&#xff09;目录通常用于存放一些通用的、不特定于任何模块的工具类或辅助函数。这些工具类或函数为整个应用程序或多个模块提供便利的功能支持&#xff0c;使得代码更加模块化、易于维护和重用。Utils目…

Echarts 柱状图中每个柱状图如何自定义展示内容

文章目录 需求分析需求 分析 要自定义柱状图中每个柱子的展示内容,您可以通过设置 label 的 formatter 属性来实现。formatter 是一个回调函数,可以用来自定义 label 的显示内容。以下是一个示例代码,演示了如何实现这一点: <!DOCTYPE html> <html lang="e…

【quarks系列】基于Dockerfile构建native镜像

目录 Dockerfile构建代码测试 Dockerfile FROM quay.io/quarkus/ubi-quarkus-native-image:22.3-java11 AS buildWORKDIR /workspace COPY . .RUN ./mvnw -DskipTeststrue clean package -Dnative -U# Stage 2: Create the minimal runtime image FROM registry.access.redhat…

AWS的EC2之间ping不通,服务之间不通,怎么办

AWS启动的两个EC2实例&#xff0c;互相访问不了 修改安全组规则&#xff0c;添加ICMP 流量的入站规则 参考&#xff1a;AWS的EC2之间ping不通,服务之间不通,怎么办_aws ec2同一个区域的服务器-CSDN博客

RabbitMQ支持的消息模型

RabbitMQ基础RabbitMQ支持的消息模型 一、第一种模型(直连) 我们将用Java编写两个程序&#xff0c;发送单个消息的生成者和接收消息并打印出来的消费者。 在下图&#xff0c;“P”是生成者&#xff0c;“C”消费者。中间框是一个队列RabbitMQ保留的消息缓冲区 。 首先构建一个…

思维,1209G1 - Into Blocks (easy version)

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 Problem - 1209G1 - Codeforces 二、解题报告 1、思路分析 考虑&#xff1a; 最终状态为若干段相同数字&#xff0c;且任意两段数字不同 每个数字出现的最左下标和最右下标构成一个区间 连锁反应—…

算法金 | 10 大必知的自动化机器学习库(Python)

大侠幸会&#xff0c;在下全网同名[算法金] 0 基础转 AI 上岸&#xff0c;多个算法赛 Top [日更万日&#xff0c;让更多人享受智能乐趣] 一、入门级自动化机器学习库 1.1 Auto-Sklearn 简介&#xff1a; Auto-Sklearn 是一个自动机器学习库&#xff0c;基于 Python 的 scikit…

【网络编程开发】4.socket套接字及TCP的实现框架 5.TCP多进程并发

4.socket套接字及TCP的实现框架 Socket套接字 Socket套接字是网络编程中用于实现不同计算机之间通信的一个基本构建块。 在现代计算机网络中&#xff0c;Socket套接字扮演着至关重要的角色。它们为应用程序提供了一种方式&#xff0c;通过这种方式&#xff0c;程序能够通过网…

2024年城市建设与环境管理国际会议(ICUCEM 2024)

2024 International Conference on Urban Construction and Environmental Management 【1】大会信息 大会地点&#xff1a;中国成都 投稿邮箱&#xff1a;icucemsub-paper.com 【2】会议简介 2024年城市建设与环境管理国际会议是一个专注于探讨城市建设与环境管理前沿议题…

EasyRecovery2024终极破解指南来袭!

在数字化时代&#xff0c;数据成为了每个人、每家公司最宝贵的资产之一。无论是个人的照片、文档&#xff0c;还是公司的机密信息&#xff0c;一旦丢失&#xff0c;后果不堪设想。因此&#xff0c;数据恢复工具如EasyRecovery2024应运而生&#xff0c;成为了保护数据安全的利器…

kubeedge v1.17.0部署教程

文章目录 前言一、安装k8s平台二、部署kubeedge1.部署MetalLB(可选)2.cloud3.edge4. 部署nginx到edge端 总结参考 前言 本文主要介绍kubeedge v1.17.0的安装过程 主要环境如下表 应用版本centos7.0k8s1.28.2kubeedge1.17.0docker24.0.8centos7.0 一、安装k8s平台 本文主要参…

Doris Connector 结合 Flink CDC 实现 MySQL 分库分表

1. 概述 在实际业务系统中为了解决单表数据量大带来的各种问题&#xff0c;我们通常采用分库分表的方式对库表进行拆分&#xff0c;以达到提高系统的吞吐量。 但是这样给后面数据分析带来了麻烦&#xff0c;这个时候我们通常试将业务数据库的分库分表同步到数据仓库时&#x…

英伟达Docker 安装与GPu镜像拉取

获取nvidia_docker压缩包nvidia_docker.tgz将压缩包上传至服务器指定目录解压nvidia_docker.tgz压缩包 tar -zxvf 压缩包执行rpm安装命令&#xff1a; #查看指定rpm包安装情况 rpm -qa | grep libstdc #查看指定rpm包下的依赖包的版本情况 strings /lib64/libstdc |grep GLI…

【代码随想录】【算法训练营】【第25天】 [216]组合总和III [17] 电话号码的字母组合

前言 思路及算法思维&#xff0c;指路 代码随想录。 题目来自 LeetCode。 day 25&#xff0c;周六&#xff0c;坚持有点困难~ 题目详情 [216] 组合总和III 题目描述 216 组合总和III 解题思路 前提&#xff1a;组合子集问题 思路&#xff1a;回溯算法&#xff0c;剪枝…

347. 前 K 个高频元素

题目 给你一个整数数组 nums 和一个整数 k &#xff0c;请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。 示例 1: 输入: nums [1,1,1,2,2,3], k 2 输出: [1,2] 示例 2: 输入: nums [1], k 1 输出: [1] 提示&#xff1a; 1 < nums.length < 1…

基于聚类与统计检验深度挖掘电商用户行为

1.项目背景 在当今竞争激烈的电商市场中,了解用户的行为和需求对于制定成功的市场策略至关重要,本项目通过建立RFM模型、K-Means聚类模型,将1000个用户进行划分,针对不同类的用户,提出不同的营销策略,最后通过统计检验来探究影响用户消费行为的因素和影响用户上网行为的…