文章目录
- 前言
- 一、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 值只能是 int、boolean、float、long、String、StringSet 这些类型的数据。
作为 Android 原生库中自带的轻量级存储类,SP 在使用方式上还是很便捷的,但是也存在以下的一些问题:
- 通过 getSharedPreferences() 方法获取 SP 实例对象,从首次初始化到读到数据会存在延迟,因为读文件操作需阻塞调用的线程直到文件读取完毕,因此不要在主线程调用,可能会对 UI 界面的流畅度造成影响。(线程阻塞)
- SP 在跨进程共享方面无法保证线程安全,因此在 Android 7.0 之后便不再对跨进程模式进行支持。(跨进程共享)
- 将数据写入到 xml 文件需要经过两次数据拷贝,如果数据量过大,将会有很大的性能损耗,效率不高。(两次拷贝)
为了解决上述问题,腾讯的微信团队基于 MMAP 研发了 MMKV 来代替 SP。
一、MMKV简介
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。支持通过 AES 算法对 protobuf 文件进行加密,并且引入 循环冗余校验码(CRC) 对文件的完整性进行校验。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Windows 平台,并且开源。
1.mmap
mmap 是 memory 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_READ、PROT_WRITE、PROT_EXEC等。
- flags:标志位,可以控制映射区域的特性。常见的有 MAP_SHARED 和 MAP_PRIVATE 等。
- fd:文件描述符,用于指定映射的文件 (由 open( ) 函数返回)。
- offset:映射的起始位置,表示被映射对象 (即文件) 从那里开始对映,通常设置为 0,该值应该为大小为PAGE_SIZE(4K)的整数倍。
mmap 函数会将一个文件或其他对象映射到进程的地址空间中,并返回一个指向映射区域的指针,进程可以使用指针来访问映射区域的数据,就像访问内存一样。关于 mmap 的映射原理及源码分析,有兴趣的同学可看一下这篇文章。
2.protobuf
Protocol Buffers 简称:Protobuf,是 Google 提供的一个具有高效的协议数据交换格式工具库,用于高效地序列化和反序列化结构化数据,通常用于网络通信、数据存储等场景。
Protobuf 和 Xml、Json 序列化的方式不同,采用了二进制字节的序列化方式,用字段索引和字段类型通过算法计算得到字段之间的关系映射,从而达到更高的时间效率和空间效率,特别适合对数据大小和传输速率比较敏感的场合使用。
Protobuf 采用了一种 TLV (Tag-Length-Value) 的格式进行编码,其格式如下:
由图可知,每条字段都由 Tag、Length、Value 三部分组成,其中 Length 是可选的。Tag 字段又由 field_number 和 wire_type 两部分组成,其中:
- field_number:message 定义字段时指定的字段编号;
- wire_type:Protobuf 编码类型,由三位 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::decodeMap 将 MMKV 存储的 Protobuf 文件反序列化为对应的 Map,也可以通过 MiniPBCoder::encodeDataWithObject 将 Map 序列化为对应存储的字节流。
二、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 方法对 mmapID 及 relativePath 进行结合生成对应的 mmapKey,它会将它们两者的结合经过 md5 加密从而生成对应的 key,主要目的是为了支持不同相对路径下的同名 mmapID;
- 通过 mmapKey 在 g_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 类继承了 SharedPreferences 及 SharedPreferences.Editor 接口并实现了一系列如 putInt、putLong、putString 等方法,同时也有 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);
}