IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 存储优化 —— MMKV 集成与原理,一篇文章帮你解答 -> 正文阅读

[移动开发]Android 存储优化 —— MMKV 集成与原理,一篇文章帮你解答

// 添加/更新数据
kv.encode(key, value);

// 获取数据
int tmp = kv.decodeInt(key);

// 删除数据
kv.removeValueForKey(key);


#### SP 的迁移

private void testImportSharedPreferences() {
MMKV mmkv = MMKV.mmkvWithID(“myData”);
SharedPreferences old_man = getSharedPreferences(“myData”, MODE_PRIVATE);
// 迁移旧数据
mmkv.importFromSharedPreferences(old_man);
// 清空旧数据
old_man.edit().clear().commit();

}


#### 数据测试

以下是 MMKV、SharedPreferences 和 SQLite 同步写入 1000 条数据的测试结果

// MMKV
MMKV: MMKV write int: loop[1000]: 12 ms
MMKV: MMKV read int: loop[1000]: 3 ms

MMKV: MMKV write String: loop[1000]: 7 ms
MMKV: MMKV read String: loop[1000]: 4 ms

// SharedPreferences
MMKV: SharedPreferences write int: loop[1000]: 119 ms
MMKV: SharedPreferences read int: loop[1000]: 3 ms

MMKV: SharedPreferences write String: loop[1000]: 187
MMKV: SharedPreferences read String: loop[1000]: 2 ms

// SQLite
MMKV: sqlite write int: loop[1000]: 101 ms
MMKV: sqlite read int: loop[1000]: 136 ms

MMKV: sqlite write String: loop[1000]: 29 ms
MMKV: sqlite read String: loop[1000]: 93 ms


可以看到 MMKV 无论是对比 SP 还是 SQLite, 在性能上都有非常大的优势, 官方提供的数据测试结果如下

![单进程读写性能对比](https://user-gold-cdn.xitu.io/2019/8/15/16c94ab9035bb3d7?imageView2/0/w/1280/h/960/ignore-error/1)

更详细的性能测试见 [wiki]( )

了解 MMKV 的使用方式和测试结果, 让我对其实现原理产生了很大的好奇心, 接下来便看看它是如何将性能做到这个地步的, 这里对主要对 MMKV 的基本操作进行剖析

*   初始化
*   实例化
*   encode
*   decode
*   进程读写的同步

我们从初始化的流程开始分析

二. 初始化
------

public class MMKV implements SharedPreferences, SharedPreferences.Editor {

// call on program start
public static String initialize(Context context) {
    String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
    return initialize(root, null);
}

static private String rootDir = null;

public static String initialize(String rootDir, LibLoader loader) {
    ...... // 省略库文件加载器相关代码
    // 保存根目录
    MMKV.rootDir = rootDir;
    // Native 层初始化
    jniInitialize(MMKV.rootDir);
    return rootDir;
}

private static native void jniInitialize(String rootDir);

}


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

// native-bridge.cpp
namespace mmkv {

MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir) {
if (!rootDir) {
return;
}
const char *kstr = env->GetStringUTFChars(rootDir, nullptr);
if (kstr) {
MMKV::initializeMMKV(kstr);
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() {
// 1.1 获取一个 unordered_map, 类似于 Java 中的 HashMap
g_instanceDic = new unordered_map<std::string, MMKV *>;
// 1.2 初始化线程锁
g_instanceLock = ThreadLock();

}

void MMKV::initializeMMKV(const std::string &rootDir) {
// 由 Linux Thread 互斥锁和条件变量保证 initialize 函数在一个进程内只会执行一次
// https://blog.csdn.net/zhangxiao93/article/details/51910043
static pthread_once_t once_control = PTHREAD_ONCE_INIT;
// 1. 进行初始化操作
pthread_once(&once_control, initialize);
// 2. 将根目录保存到全局变量
g_rootDir = rootDir;
// 拷贝字符串
char *path = strdup(g_rootDir.c_str());
if (path) {
// 3. 根据路径, 生成目标地址的目录
mkPath(path);
// 释放内存
free(path);
}
}


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

*   pthread\_once\_t: 类似于 Java 的单例, 其 initialize 方法在进程内只会执行一次
    *   创建 MMKV 对象的缓存散列表 g\_instanceDic
    *   创建一个线程锁 g\_instanceLock
*   mkPath: 根据字符串创建文件目录

接下来我们看看这个目录创建的过程

### 目录的创建

// 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;

}


以上是 Native 层创建文件路径的通用代码, 逻辑很清晰

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

三. 实例化
------

public class MMKV implements SharedPreferences, SharedPreferences.Editor {

@Nullable
public static MMKV mmkvWithID(String mmapID, int mode, String cryptKey, String relativePath) {
    ......
    // 执行 Native 初始化, 获取句柄值
    long handle = getMMKVWithID(mmapID, mode, cryptKey, relativePath);
    if (handle == 0) {
        return null;
    }
    // 构建一个 Java 的壳对象
    return new MMKV(handle);
}

private native static long
getMMKVWithID(String mmapID, int mode, String cryptKey, String relativePath);

// jni
private long nativeHandle;

private MMKV(long handle) {
    nativeHandle = handle;
}

}


可以看到 MMKV 实例构建的主要逻辑通过 getMMKVWithID 方法实现, 看它内部做了什么

// 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) {
return (jlong) kv;
}
// 获取独立存储 id
string str = jstring2string(env, mmapID);

bool done = false;
if (cryptKey) {
    // 获取秘钥
    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;
    }
}
......
// 强转成句柄, 返回到 Java
return (jlong) kv;

}

}


可以看到最终通过 MMKV::mmkvWithID 函数获取到 MMKV 的对象

// MMKV.cpp
MMKV *MMKV::mmkvWithID(
const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {

if (mmapID.empty()) {
    return nullptr;
}
SCOPEDLOCK(g_instanceLock);
// 1. 通过 mmapID 和 relativePath, 组成最终的 mmap 文件路径的 key
auto mmapKey = mmapedKVKey(mmapID, relativePath);
// 2. 从全局缓存中查找
auto itr = g_instanceDic->find(mmapKey);
if (itr != g_instanceDic->end()) {
    MMKV *kv = itr->second;
    return kv;
}
// 3. 创建缓存文件
if (relativePath) {
    // 根据 mappedKVPathWithID 获取 mmap 的最终文件路径
    // mmapID 使用 md5 加密
    auto filePath = mappedKVPathWithID(mmapID, mode, relativePath);
    // 不存在则创建一个文件
    if (!isFileExist(filePath)) {
        if (!createFile(filePath)) {
            return nullptr;
        }
    }
    ......
}
// 4. 创建实例对象
auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath);
// 5. 缓存这个 mmapKey
(*g_instanceDic)[mmapKey] = kv;
return kv;

}


mmkvWithID 函数的实现流程非常的清晰, 这里我们主要关注一下实例对象的创建流程

// MMKV.cpp
MMKV::MMKV(
const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath)
: m_mmapID(mmapedKVKey(mmapID, relativePath))
// 拼装文件的路径
, m_path(mappedKVPathWithID(m_mmapID, mode, relativePath))
// 拼装 .crc 文件路径
, m_crcPath(crcPathWithID(m_mmapID, mode, relativePath))
// 1. 将文件摘要信息映射到内存, 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 加解密的引擎
if (cryptKey && cryptKey->length() > 0) {
m_crypter = new AESCrypt((const unsigned char *) cryptKey->data(), cryptKey->length());
}

// sensitive zone
{
SCOPEDLOCK(m_sharedProcessLock);
// 2. 根据 m_mmapID 来加载文件中的数据
loadFromFile();
}
}


可以从 MMKV 的构造函数中看到很多有趣的信息, **MMKV 是支持 Ashmem 共享内存的, 当我们不想将文件写入磁盘,但是又想进行跨进程通信,就可以使用 MMKV 提供的 MMAP\_ASHMEM**

不过这里我们主要关注两个关键点

*   m\_metaFile 文件摘要的映射
*   loadFromFile 数据的载入

接下来我们先看看, 文件摘要信息的映射

### 一) 文件摘要的映射

// 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) {
// 1. 打开文件
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 {
// 2. 创建文件锁
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);
}
// 3. 验证文件的大小是否小于一个内存页, 一般为 4kb
if (m_segmentSize < DEFAULT_MMAP_SIZE) {
m_segmentSize = static_cast<size_t>(DEFAULT_MMAP_SIZE);
// 3.1 通过 ftruncate 将文件大小对其到内存页
// 3.2 通过 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;
}
}
// 4. 通过 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 的构造函数处理的事务如下

*   打开指定的文件
*   创建这个文件锁
*   修正文件大小, 最小为 4kb
    *   前 4kb 用于统计数据总大小
*   通过 mmap 将文件映射到内存

好的, 通过 MmapedFile 的构造函数, 我们便能够获取到映射后的内存首地址了, 操作这块内存时 Linux 内核会负责将内存中的数据同步到文件中

比起 SP 的数据同步, mmap 显然是要优雅的多, **即使进程意外死亡, 也能够通过 Linux 内核的保护机制, 将进行了文件映射的内存数据刷入到文件中, 提升了数据写入的可靠性**

结下来看看数据的载入

### 二) 数据的载入

// 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 {
    // 1. 获取文件大小
    m_size = 0;
    struct stat st = {0};
    if (fstat(m_fd, &st) != -1) {
        m_size = static_cast<size_t>(st.st_size);
    }
    // 1.1 将文件大小对其到内存页的整数倍
    if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
        ......
    }
    // 2. 获取文件映射后的内存地址
    m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
    if (m_ptr == MAP_FAILED) {
        ......
    } else {
        // 3. 读取内存文件的前 32 位, 获取存储数据的真实大小
        memcpy(&m_actualSize, m_ptr, Fixed32Size);
        ......
        bool loadFromFile = false, needFullWriteback = false;
        if (m_actualSize > 0) {
            // 4. 验证文件的长度
            if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {
                // 5. 验证文件 CRC 的正确性
                if (checkFileCRCValid()) {
                    loadFromFile = true;
                } else {
                    // 若不正确, 则回调异常 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;
                }
            }
        }
        // 6. 需要从文件获取数据
        if (loadFromFile) {
            ......
            // 构建输入缓存
            MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
            if (m_crypter) {
                // 解密输入缓冲中的数据
                decryptBuffer(*m_crypter, inputBuffer);
            }
            // 从输入缓冲中将数据读入 m_dic
            m_dic.clear();
            MiniPBCoder::decodeMap(m_dic, inputBuffer);
            // 构建输出数据
            m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize,
                                           m_size - Fixed32Size - m_actualSize);
            // 进行重整回写, 剔除重复的数据
            if (needFullWriteback) {
                fullWriteback();
            }
        } 
        // 7. 说明文件中没有数据, 或者校验失败了
        else {
            SCOPEDLOCK(m_exclusiveProcessLock);
            // 清空文件中的数据
            if (m_actualSize > 0) {
                writeAcutalSize(0);
            }
            m_output = new CodedOutputData(m_ptr + Fixed32Size, m_size - Fixed32Size);
            // 重新计算 CRC
            recaculateCRCDigest();
        }
        ......
    }
}

......

m_needLoadFromFile = false;

}


好的, 可以看到 loadFromFile 中对于 CRC 验证通过的文件, 会将文件中的数据读入到 m\_dic 中缓存, 否则则会清空文件

*   因此用户恶意修改文件之后, 会破坏 CRC 的值, 这个存储数据便会被作废, 这一点要尤为注意
*   **从文件中读取数据到 m\_dic 之后, 会将 mdic 回写到文件中**, 其重写的目的是为了剔除重复的数据
    *   关于为什么会出现重复的数据, 在后面 encode 操作中再分析

### 三) 回顾

到这里 MMKV 实例的构建就完成了, 有了 m\_dic 这个内存缓存, 我们进行数据查询的效率就大大提升了

从最终的结果来看它与 SP 是一致的, 都是初次加载时会将文件中所有的数据加载到散列表中, 不过 MMKV 多了一步数据回写的操作, 因此当数据量比较大时, 对实例构建的速度有一定的影响

// 写入 1000 条数据之后, MMVK 和 SharedPreferences 实例化的时间对比
E/TAG: create MMKV instance time is 4 ms
E/TAG: create SharedPreferences instance time is 1 ms


从结果上来看, MMVK 的确在实例构造速度上有一定的劣势, 不过得益于是将 m\_dic 中的数据写入到 mmap 的内存, 其真正进行文件写入的时机由 Linux 内核决定, 再加上文件的页缓存机制, 所以速度上虽有劣势, 但不至于无法接受

四. encode
---------

关于 **encode 即数据的添加与更新**的流程, 这里以 encodeString 为例

public class MMKV implements SharedPreferences, SharedPreferences.Editor {

public boolean encode(String key, String value) {
    return encodeString(nativeHandle, key, value);
}

private native boolean encodeString(long handle, String key, String value);

}


看看 native 层的实现

// native-bridge.cpp
namespace mmkv {

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);
// 若是 value 非 NULL
if (oValue) {
// 通过 setStringForKey 函数, 将数据存入
string value = jstring2string(env, oValue);
return (jboolean) kv->setStringForKey(value, key);
}
// 若是 value 为 NULL, 则移除 key 对应的 value 值
else {
kv->removeValueForKey(key);
return (jboolean) true;
}
}
return (jboolean) false;
}

}


这里我们主要分析一下 setStringForKey 这个函数

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


这里主要分为两步操作

*   数据编码
*   更新键值对

### 一) 数据的编码

MMKV 采用的是 ProtocolBuffer 编码方式, 这里就不做过多介绍了, 具体请查看 [Google 官方文档]( )

// MiniPBCoder.cpp
MMBuffer MiniPBCoder::getEncodeData(const string &str) {
// 1. 创建编码条目的集合
m_encodeItems = new vector();
// 2. 为集合填充数据
size_t index = prepareObjectForEncode(str);
PBEncodeItem *oItem = (index < m_encodeItems->size()) ? &(*m_encodeItems)[index] : nullptr;
if (oItem && oItem->compiledSize > 0) {
// 3. 开辟一个内存缓冲区, 用于存放编码后的数据
m_outputBuffer = new MMBuffer(oItem->compiledSize);
// 4. 创建一个编码操作对象
m_outputData = new CodedOutputData(m_outputBuffer->getPtr(), m_outputBuffer->length());
// 执行 protocolbuffer 编码, 并输出到缓冲区
writeRootObject();
}
// 调用移动构造函数, 重新创建实例返回
return move(*m_outputBuffer);
}

size_t MiniPBCoder::prepareObjectForEncode(const string &str) {
// 2.1 创建 PBEncodeItem 对象用来描述待编码的条目, 并添加到 vector 集合
m_encodeItems->push_back(PBEncodeItem());
// 2.2 获取 PBEncodeItem 对象
PBEncodeItem *encodeItem = &(m_encodeItems->back());
// 2.3 记录索引位置
size_t index = m_encodeItems->size() - 1;
{
// 2.4 填充编码类型
encodeItem->type = PBEncodeItemType_String;
// 2.5 填充要编码的数据
encodeItem->value.strValue = &str;
// 2.6 填充数据大小
encodeItem->valueSize = static_cast<int32_t>(str.size());
}
// 2.7 计算编码后的大小
encodeItem->compiledSize = pbRawVarint32Size(encodeItem->valueSize) + encodeItem->valueSize;
return index;
}


可以看到, 再未进行编码操作之前, 编码后的数据大小就已经确定好了, 并且将它保存在了 encodeItem->compiledSize 中, 接下来我们看看执行数据编码并输出到缓冲区的操作流程

// MiniPBCoder.cpp
void MiniPBCoder::writeRootObject() {
for (size_t index = 0, total = m_encodeItems->size(); index < total; index++) {
PBEncodeItem *encodeItem = &(m_encodeItems)[index];
switch (encodeItem->type) {
// 主要关心编码 String
case PBEncodeItemType_String: {
m_outputData->writeString(
(encodeItem->value.strValue));
break;
}

}
}
}

// CodedOutputData.cpp
void CodedOutputData::writeString(const string &value) {
size_t numberOfBytes = value.size();

// 1. 按照 varint 方式编码字符串长度, 会改变 m_position 的值
this->writeRawVarint32((int32_t) numberOfBytes);
// 2. 将字符串的数据拷贝到编码好的长度后面
memcpy(m_ptr + m_position, ((uint8_t *) value.data()), numberOfBytes);
// 更新 position 的值
m_position += numberOfBytes;
}


可以看到 CodedOutputData 的 writeString 中按照 protocol buffer 进行了字符串的编码操作

其中 m\_ptr 是上面开辟的内存缓冲区的地址, 也就是说 writeString 执行结束之后, 数据就已经被写入缓冲区了

有了编码好的数据缓冲区, 接下来看看更新键值对的操作

### 二) 键值对的更新

// MMKV.cpp
bool MMKV::setStringForKey(const std::string &value, const std::string &key) {
// 编码数据获取存放数据的缓冲区
auto data = MiniPBCoder::encodeDataWithObject(value);
// 更新键值对
return setDataForKey(std::move(data), key);
}

bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) {

// 将键值对写入 mmap 文件映射的内存中
auto ret = appendDataWithKey(data, key);
// 写入成功, 更新散列数据
if (ret) {
m_dic[key] = std::move(data);
m_hasFullWriteback = false;
}
return ret;
}

bool MMKV::appendDataWithKey(const MMBuffer &data, const std::string &key) {
// 1. 计算 key + value 的 ProtocolBuffer 编码后的长度
size_t keyLength = key.length();
size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength);
size += data.length() + pbRawVarint32Size((int32_t) data.length());
SCOPEDLOCK(m_exclusiveProcessLock);

// 2. 验证是否有足够的空间, 不足则进行数据重整与扩容操作
bool hasEnoughSize = ensureMemorySize(size);
if (!hasEnoughSize || !isFileValid()) {
    return false;
}

// 3. 更新文件头的数据总大小
writeAcutalSize(m_actualSize + size);

// 4. 将 key 和编码后的 value 写入到文件映射的内存
m_output->writeString(key);
m_output->writeData(data);

// 5. 获取文件映射内存当前 <key, value> 的起始位置
auto ptr = (uint8_t *) m_ptr + Fixed32Size + m_actualSize - size;
if (m_crypter) {
    // 加密这块区域
    m_crypter->encrypt(ptr, ptr, size);
}

// 6. 更新 CRC
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-31 15:33:43  更:2021-08-31 15:33:49 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/31 5:35:51-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码