当前位置: 首页 > news >正文

自个做网站教程网站会对特殊的ip做跳转

自个做网站教程,网站会对特殊的ip做跳转,wordpress自己新建模板,网站开发vs2013文章目录 前言一、MMKV简介1.mmap2.protobuf 二、MMKV 源码详解1.MMKV初始化2.MMKV对象获取3.文件摘要的映射4.loadFromFile 从文件加载数据5.数据写入6.内存重整7.数据读取8.数据删除9.文件回写10.Protobuf 实现1.序列化2.反序列化 12.文件锁1.加锁2.解锁 13.状态同步 总结参考… 文章目录 前言一、MMKV简介1.mmap2.protobuf 二、MMKV 源码详解1.MMKV初始化2.MMKV对象获取3.文件摘要的映射4.loadFromFile 从文件加载数据5.数据写入6.内存重整7.数据读取8.数据删除9.文件回写10.Protobuf 实现1.序列化2.反序列化 12.文件锁1.加锁2.解锁 13.状态同步 总结参考文献 前言 谈到轻量级的数据持久化在 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/ POSIX 平台并且开源。 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_SIZE4K 对齐。length待申请映射的内存区域的大小如果是匿名映射则是要映射的匿名物理内存有多大如果是文件映射则是要映射的文件区域有多大要按照 PAGE_SIZE4K 对齐。prot映射区域的保护模式。有 PROT_READ、PROT_WRITE、PROT_EXEC等。flags标志位可以控制映射区域的特性。常见的有 MAP_SHARED 和 MAP_PRIVATE 等。fd文件描述符用于指定映射的文件 (由 open( ) 函数返回)。offset映射的起始位置表示被映射对象 (即文件) 从那里开始对映通常设置为 0该值应该为大小为PAGE_SIZE4K的整数倍。 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_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::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_mapstd::string, MMKV * *g_instanceDic; static ThreadLock g_instanceLock; static std::string g_rootDir;void initialize() {// 获取一个 unordered_map, 类似于 Java 中的 HashMapg_instanceDic new unordered_mapstd::string, MMKV *;// 初始化线程锁g_instanceLock ThreadLock();g_instanceLock-initialize();...... }void MMKV::initializeMMKV(const std::string rootDir) {// 由 Linux Thread 互斥锁和条件变量保证 initialize 函数在一个进程内只会执行一次static pthread_once_t once_control PTHREAD_ONCE_INIT;// 回调 initialize() 方法进行初始化操作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_instanceLockmkPath 根据字符串创建文件目录。 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::mmkvWithIDstring 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_castsize_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_castsize_t(st.st_size); // 获取文件大小}// 验证文件的大小是否小于一个内存页, 一般为 4kbif (m_segmentSize DEFAULT_MMAP_SIZE) {m_segmentSize static_castsize_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_castsize_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 函数的执行流程如下 通过 m_metaInfo.read 方法读取元文件的数据信息内部使用 void * memcpy ( void * dest, const void * src, size_t num ) 函数该函数的作用为复制 src 所指的内存内容的前 num 个字节到 dest 所指的内存地址上。打开 m_path 路径文件并获取文件大小将文件的大小对齐到内存页的整数倍不足则补 0(与内存映射的原理有关内存映射是基于页的换入换出机制实现的)通过 mmap 函数将文件映射到内存中得到指向该内存区域的指针 m_ptr从文件中读取前 4 个字节 Fixed32Size得到存储的数据实际占用的空间 。对文件进行长度校验及 CRC 校验(循环冗余校验可以校验文件完整性)在失败的情况下会根据当前策略进行抉择如果策略是失败时恢复则继续读取并且在最后将 map 中的内容回写到文件由指向内存区域的指针 m_ptr 构造出一块用于管理 MMKV 映射内存的 MMBuffer 对象如果需要解密通过之前构造的 AESCrypt 对象进行解密由于 MMKV 使用了 Protobuf 进行序列化通过 MiniPBCoder::decodeMap 方法将 Protobuf 转换成对应的 map构造用于输出的 CodedOutputData 类实例如果需要回写 (CRC 校验或文件长度校验失败)则调用 fullWriteback 方法将 map 中的数据回写到文件。 5.数据写入 Java 层的 MMKV 类继承了 SharedPreferences 及 SharedPreferences.Editor 接口并实现了一系列如 putInt、putLong、putString 等方法同时也有 encode 的很多重载方法用于对存储的数据进行修改下面以 putInt 为例 public class MMKV implements SharedPreferences, SharedPreferences.Editor {...// 省略部分代码 public boolean encode(String key, int value) {// 调用 Native 层的 encodeInt 方法对数据进行写入操作return encodeInt(nativeHandle, key, value);}Overridepublic Editor putInt(String key, int value) {// 调用 Native 层的 encodeInt 方法对数据进行写入操作encodeInt(nativeHandle, key, value);return this;}private native boolean encodeInt(long handle, String key, int value); }putInt 方法和 encode 方法都是调用 Native 层的 encodeInt 方法对数据进行写入操作。 // native-bridge.cpp namespace mmkv {MMKV_JNI jboolean encodeInt(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint value) {// 将 Java 层持有的 NativeHandle 转为对应的 MMKV 对象MMKV *kv reinterpret_castMMKV *(handle);if (kv oKey) {string key jstring2string(env, oKey);return (jboolean) kv-setInt32(value, key);}return (jboolean) false; } }继续调用 MMKV::setInt32 函数将数据写入 // MMKV.cpp bool MMKV::setInt32(int32_t value, const std::string key) {if (key.empty()) {return false;}// 构造值对应的 MMBuffer通过 CodedOutputData 将其写入 Buffer size_t size pbInt32Size(value); MMBuffer data(size);CodedOutputData output(data.getPtr(), size);output.writeInt32(value);return setDataForKey(std::move(data), key); }获取准备写入的 value 值在 Protobuf 中所占据的大小 size之后为其构造了对应的 MMBuffer 并将数据写入了这段 Buffer最后调用 setDataForKey 方法; CodedOutputData是与 Buffer 交互的桥梁可以通过它实现向 MMBuffer 中写入数据。 // MMKV.cpp bool MMKV::setDataForKey(MMBuffer data, const std::string key) {if (data.length() 0 || key.empty()) {return false;}SCOPEDLOCK(m_lock); // 获取写锁SCOPEDLOCK(m_exclusiveProcessLock);checkLoadData(); // 确保数据已正确读入内存 // 将键值对写入 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) {size_t keyLength key.length();// size needed to encode the key -- 计算 key 的 ProtocolBuffer 编码后的长度size_t size keyLength pbRawVarint32Size((int32_t) keyLength);// size needed to encode the value -- 计算 key value 的 ProtocolBuffer 编码后的长度size data.length() pbRawVarint32Size((int32_t) data.length());// 要写入获取写锁 SCOPEDLOCK(m_exclusiveProcessLock);// 验证是否有足够的空间, 不足则进行数据重整与扩容操作bool hasEnoughSize ensureMemorySize(size);if (!hasEnoughSize || !isFileValid()) {return false;}// m_actualSize 是位于文件的首部保存当前有效内存的大小// 由于新增数据需调用 writeAcutalSize 函数更新 m_actualSize 值writeAcutalSize(m_actualSize size);// 将编码后的 key 和 value 写入到文件映射的内存m_output-writeString(key);m_output-writeData(data); // note: write size of data// 获取文件映射内存当前 key, value 的起始位置auto ptr (uint8_t *) m_ptr Fixed32Size m_actualSize - size;if (m_crypter) { // 需加密则加密这块区域m_crypter-encrypt(ptr, ptr, size);}updateCRCDigest(ptr, size, KeepSequence); // 更新 CRC 校验码return true; }首先计算准备写入到映射空间的内容大小随后调用 ensureMemorySize 方法验证是否有足够的映射空间不足则进行数据重整与扩容操作。由于新增数据因此需调用 writeAcutalSize 函数更新 m_actualSize 值然后通过 CodedOutputData 写入编码后的数据最后更新 CRC 校验码。 注意由于 Protobuf 不支持增量更新为了避免全量写入带来的性能问题MMKV 在文件中的写入并不是通过修改文件对应的位置而是直接在后面 append 新增一条新的数据即使是修改了已存在的 key。而读取时只记录最后一条对应 key 的数据这样会导致在文件中存在冗余的数据。这样设计的原因我认为是出于性能的考量MMKV 中存在着一套内存重整机制用于对冗余的 key-value 数据进行处理其正是在确保内存充足时实现的。 6.内存重整 // MMKV.cpp bool MMKV::ensureMemorySize(size_t newSize) {if (!isFileValid()) {MMKVWarning([%s] file not valid, m_mmapID.c_str());return false;}// make some room for placeholder -- 为占位符留出一些空间constexpr size_t ItemSizeHolderSize 4; if (m_dic.empty()) {newSize ItemSizeHolderSize;}// 如果文件剩余空闲空间少于新的键值对或存储的散列表为空if (newSize m_output-spaceLeft() || m_dic.empty()) {// try a full rewrite to make space --- 尝试完全重写以腾出空间static const int offset pbFixed32Size(0);// 通过 MiniPBCoder::encodeDataWithObject 将整个 map 转换为对应的 MMBufferMMBuffer data MiniPBCoder::encodeDataWithObject(m_dic);// 计算所需的空间大小size_t lenNeeded data.length() offset newSize;if (m_isAshmem) {if (lenNeeded m_size) {MMKVError(ashmem %s reach size limit:%zu, consider configure with larger size,m_mmapID.c_str(), m_size);return false;}} else {// 计算每个键值对的平均大小size_t avgItemSize lenNeeded / std::maxsize_t(1, m_dic.size());// 计算未来可能会使用的空间大小size_t futureUsage avgItemSize * std::maxsize_t(8, (m_dic.size() 1) / 2);// 1. no space for a full rewrite, double it -- 所需空间大小 当前可映射文件总大小// 2. or space is not large enough for future usage, double it to avoid frequently full rewrite// 如果内存重整后仍不足以写入则将大小不断乘2直至足够写入最后通过 mmap 重新映射文件 if (lenNeeded m_size || (lenNeeded futureUsage) m_size) {size_t oldSize m_size;do {m_size * 2; // double 空间直至足够} while (lenNeeded futureUsage m_size);MMKVInfo(extending [%s] file size from %zu to %zu, incoming size:%zu, future usage:%zu,m_mmapID.c_str(), oldSize, m_size, newSize, futureUsage);// if we cant extend size, rollback to old state -- 如果扩展后大小还是不够则回到之前的状态if (ftruncate(m_fd, m_size) ! 0) {MMKVError(fail to truncate [%s] to size %zu, %s, m_mmapID.c_str(), m_size,strerror(errno));m_size oldSize;return false;}if (!zeroFillFile(m_fd, oldSize, m_size - oldSize)) { // 用零填充不足部分 MMKVError(fail to zeroFile [%s] to size %zu, %s, m_mmapID.c_str(), m_size,strerror(errno));m_size oldSize;return false;}if (munmap(m_ptr, oldSize) ! 0) { // 如果扩容后满足所需则先 munmap 解除之前的映射MMKVError(fail to munmap [%s], %s, m_mmapID.c_str(), strerror(errno));}// 重新通过 mmap 映射新的空间大小m_ptr (char *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);if (m_ptr MAP_FAILED) {MMKVError(fail to mmap [%s], %s, m_mmapID.c_str(), strerror(errno));}// check if we fail to make more spaceif (!isFileValid()) {MMKVWarning([%s] file not valid, m_mmapID.c_str());return false;}}}if (m_crypter) { // 加密数据 m_crypter-reset();auto ptr (unsigned char *) data.getPtr();m_crypter-encrypt(ptr, ptr, data.length());}// 重新构建并写入数据writeAcutalSize(data.length());delete m_output;m_output new CodedOutputData(m_ptr offset, m_size - offset);m_output-writeRawData(data);recaculateCRCDigest();m_hasFullWriteback true;}return true; }当剩余映射空间不足以写入新的待写入内容时尝试进行内存重整内存重整会将文件清空将 map 中的数据重新写入文件从而去除冗余数据若内存重整后的映射空间仍然不足则不断将映射空间 double 直到足够然后 munmap 解除之前的映射调用 mmap 重新映射 7.数据读取 类似上面数据存入这里通过 getInt 函数对数据进行读取 public class MMKV implements SharedPreferences, SharedPreferences.Editor {...// 省略部分代码 public int decodeInt(String key, int defaultValue) {// 调用 Native 层的 decodeInt 方法对数据进行读取操作return decodeInt(nativeHandle, key, defaultValue);}Overridepublic int getInt(String key, int defValue) {// 调用 Native 层的 decodeInt 方法对数据进行读取操作return decodeInt(nativeHandle, key, defValue);}private native int decodeInt(long handle, String key, int defaultValue); }getInt 方法和 decodeInt 方法都是调用 Native 层的 decodeInt 方法来读取数据。 // native-bridge.cpp namespace mmkv {MMKV_JNI jint decodeInt(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint defaultValue) {// 将 Java 层持有的 NativeHandle 转为对应的 MMKV 对象MMKV *kv reinterpret_castMMKV *(handle);if (kv oKey) {string key jstring2string(env, oKey);return (jint) kv-getInt32ForKey(key, defaultValue);}return defaultValue; } }继续调用 MMKV::getInt32ForKey 函数读取数据 // MMKV.cpp int32_t MMKV::getInt32ForKey(const std::string key, int32_t defaultValue) {if (key.empty()) {return defaultValue;}SCOPEDLOCK(m_lock);auto data getDataForKey(key);if (data.length() 0) {CodedInputData input(data.getPtr(), data.length());return input.readInt32();}return defaultValue; }const MMBuffer MMKV::getDataForKey(const std::string key) {checkLoadData(); // 确保数据已正确读入内存 auto itr m_dic.find(key); // 从散列表 map 中寻找获取 key 对应的 valueif (itr ! m_dic.end()) {return itr-second;}static MMBuffer nan(0);return nan; }MMKV::getInt32ForKey 函数继续调用 getDataForKey 方法获取 key 对应的 MMBuffer如读取到数据则调用 CodedInputData 函数将数据读出并返回如长度为 0 则视为不存在对应的数据返回默认值。 getDataForKey 函数通过在散列表 map 中寻找获取 key 对应的 value找不到会返回 size 为 0 的 MMBuffer。 注意MMKV 读写是直接读写到 mmap 文件映射的内存上绕开了普通读写 io 需要进入内核再写到磁盘的过程。 8.数据删除 通过 Java 层 MMKV 的 remove、removeValueForKey 方法传入指定的 key 值来实现删除操作 public class MMKV implements SharedPreferences, SharedPreferences.Editor {...// 省略部分代码 public void removeValueForKey(String key) {// 调用 Native 层的 removeValueForKey 方法对数据进行删除操作removeValueForKey(nativeHandle, key);}Overridepublic Editor remove(String key) {// 调用 Native 层的 removeValueForKey 方法对数据进行删除操作removeValueForKey(key);return this;}private native void removeValueForKey(long handle, String key); }remove 方法和 removeValueForKey 方法都是调用 Native 层的 removeValueForKey 方法来删除数据。 // native-bridge.cpp namespace mmkv {MMKV_JNI void removeValueForKey(JNIEnv *env, jobject instance, jlong handle, jstring oKey) {// 将 Java 层持有的 NativeHandle 转为对应的 MMKV 对象MMKV *kv reinterpret_castMMKV *(handle);if (kv oKey) {string key jstring2string(env, oKey);kv-removeValueForKey(key);} } }继续调用 MMKV::removeValueForKey 函数删除数据 // MMKV.cpp void MMKV::removeValueForKey(const std::string key) {if (key.empty()) {return;}SCOPEDLOCK(m_lock);SCOPEDLOCK(m_exclusiveProcessLock);checkLoadData(); // 确保数据已正确读入内存removeDataForKey(key); }bool MMKV::removeDataForKey(const std::string key) {if (key.empty()) {return false;}// 从散列表 map 中删除 key 对应的 valueauto deleteCount m_dic.erase(key);if (deleteCount 0) {m_hasFullWriteback false;static MMBuffer nan(0);return appendDataWithKey(nan, key);}// 读取时发现它的 size 为 0则会认为这条数据已经删除;return false; }removeDataForKey 函数从散列表 map 中删除 key 对应的 value然后构造一条 size 为 0 的 MMBuffer 并调用 appendDataWithKey 函数将其 append 到 Protobuf 文件中。 9.文件回写 MMKV 中在一些特定的情景下会通过 fullWriteback 方法立即将散列表 map 中的数据回写到文件。如 通过 MMKV.reKey 方法修改加密的 key通过 MMKV.removeValuesForKeys 删除一系列 key读取文件时文件校验或 CRC 冗余校验失败。 // MMKV.cpp bool MMKV::fullWriteback() {if (m_hasFullWriteback) { // 如果已经回写完毕则返回 truereturn true;}if (m_needLoadFromFile) { // 需要从文件中加载数据是则返回 true即此时暂时不回写return true;}if (!isFileValid()) { // 文件不可用即没有文件可供回写则返回 falseMMKVWarning([%s] file not valid, m_mmapID.c_str());return false;}if (m_dic.empty()) { // 如果存储所用的散列表 m_dic 是空的直接清空文件clearAll();return true;}// 将 m_dic 转换为对应的 MMBufferauto allData MiniPBCoder::encodeDataWithObject(m_dic);SCOPEDLOCK(m_exclusiveProcessLock);if (allData.length() 0) {if (allData.length() Fixed32Size m_size) { // 如果空间足够写入则直接写入if (m_crypter) { // 加密数据m_crypter-reset();auto ptr (unsigned char *) allData.getPtr();m_crypter-encrypt(ptr, ptr, allData.length());}writeAcutalSize(allData.length());delete m_output;// 通过 CodedOutputData 写入编码后的数据m_output new CodedOutputData(m_ptr Fixed32Size, m_size - Fixed32Size);m_output-writeRawData(allData); // note: dont write size of datarecaculateCRCDigest(); // 更新 CRC 校验码m_hasFullWriteback true; // 回写完毕标记 m_hasFullWriteback 为 truereturn true;} else {// 如果剩余空间不够写入则调用 ensureMemorySize 函数进行内存重整与扩容return ensureMemorySize(allData.length() Fixed32Size - m_size);}}return false; }如果存储所用的散列表 m_dic 是空的表示所有的数据已被删除通过 clearAll 函数清除文件与数据然后重新从文件中加载数据如果当前映射空间足够写入散列表 m_dic 中回写的数据则直接通过 CodedOutputData 写入编码后的数据否者调用 ensureMemorySize 函数进行内存重整与扩容。 10.Protobuf 实现 在 MMKV 中通过 MiniPBCoder 完成 Protobuf 的序列化及反序列化。通过 MiniPBCoder::decodeMap 将 MMKV 存储的 Protobuf 文件反序列化为对应的散列表 map通过 MiniPBCoder::encodeDataWithObject 将 map 序列化为对应存储的字节流 MMBuffer。 1.序列化 // MiniPBCoder.h class MiniPBCoder {const MMBuffer *m_inputBuffer;CodedInputData *m_inputData;MMBuffer *m_outputBuffer;CodedOutputData *m_outputData;std::vectorPBEncodeItem *m_encodeItems;private:MiniPBCoder();MiniPBCoder(const MMBuffer *inputBuffer);...... public:template typename Tstatic MMBuffer encodeDataWithObject(const T obj) {MiniPBCoder pbcoder;return pbcoder.getEncodeData(obj);} };继续调用到 MiniPBCoder::getEncodeData 方法并传入待序列化的 map // MiniPBCoder.cpp MMBuffer MiniPBCoder::getEncodeData(const unordered_mapstring, MMBuffer map) {m_encodeItems new vectorPBEncodeItem(); // 新建 PBEncodeItem 数组// 调用 prepareObjectForEncode 方法将 map 中的键值对分别构建成 PBEncodeItem 并添加到 m_encodeItems 中size_t index prepareObjectForEncode(map);PBEncodeItem *oItem (index m_encodeItems-size()) ? (*m_encodeItems)[index] : nullptr;if (oItem oItem-compiledSize 0) {// 新建输出缓存 MMBuffer然后通过新建的 CodedOutputData 将数据写到 MMBuffer 中m_outputBuffer new MMBuffer(oItem-compiledSize);m_outputData new CodedOutputData(m_outputBuffer-getPtr(), m_outputBuffer-length());writeRootObject();}return std::move(*m_outputBuffer); }调用 MiniPBCoder::prepareObjectForEncode 方法将 map 中的键值对转为对应的 PBEncodeItem 对象数组随后构建对应的用于写出的 CodedOutputData以及输出缓存 MMBuffer最后调用 writeRootObject 方法将数据通过 CodedOutputData 写到 MMBuffer 缓存中。 首先来看 MiniPBCoder::prepareObjectForEncode 方法是如何序列化 map 的 // MiniPBCoder.cpp size_t MiniPBCoder::prepareObjectForEncode(const unordered_mapstring, MMBuffer map) {// 新建 PBEncodeItem 放到 m_encodeItems 的尾部m_encodeItems-push_back(PBEncodeItem());// 获取 m_encodeItems 尾部刚刚新建的 PBEncodeItem 的引用以及其对应的 indexPBEncodeItem *encodeItem (m_encodeItems-back());size_t index m_encodeItems-size() - 1; { // 将该 PBEncodeItem 作为一个 ContainerencodeItem-type PBEncodeItemType_Container;encodeItem-value.strValue nullptr;for (const auto itr : map) { // 遍历 mapconst auto key itr.first;const auto value itr.second;if (key.length() 0) {continue; // 如果 key 为空则跳过本次循环}// 将 key 作为一个 String 类型的 EncodeItem 放入数组size_t keyIndex prepareObjectForEncode(key);if (keyIndex m_encodeItems-size()) {// 将 value 作为一个 Data 类型存储 MMBuffer 的 EncodeItem 放入数组size_t valueIndex prepareObjectForEncode(value);if (valueIndex m_encodeItems-size()) {// 计算 Container 添加 key 和 value 后的 size(*m_encodeItems)[index].valueSize (*m_encodeItems)[keyIndex].compiledSize;(*m_encodeItems)[index].valueSize (*m_encodeItems)[valueIndex].compiledSize;} else {// 移除 m_encodeItems 中的最后一个元素m_encodeItems-pop_back(); // pop key}}}encodeItem (*m_encodeItems)[index];}encodeItem-compiledSize pbRawVarint32Size(encodeItem-valueSize) encodeItem-valueSize;return index; }首先在 m_encodeItems 的尾部添加一个作为 Container 的 PBEncodeItem随后遍历 map 并对每个 key 和 value 分别构建对应的 PBEncodeItem 并添加到 m_encodeItems 的尾部并且将它们的 size 计算入 Container 的 valueSize最后返回该 Container 在 m_encodeItems 中的 index。 接下来看 MiniPBCoder::writeRootObject 方法是如何将数据写入到缓存 MMBuffer 的 // 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) {case PBEncodeItemType_String: {m_outputData-writeString(*(encodeItem-value.strValue));break;}case PBEncodeItemType_Data: {m_outputData-writeData(*(encodeItem-value.bufferValue));break;}case PBEncodeItemType_Container: {m_outputData-writeRawVarint32(encodeItem-valueSize);break;}case PBEncodeItemType_None: {MMKVError(%d, encodeItem-type);break;}}} }根据 MiniPBCoder::prepareObjectForEncode 构建的不同类型的 PBEncodeItem分别调用 CodedOutputData 对应的写入函数进行写入操作。其中 PBEncodeItemType_Container 类型写入的就是后面数据的长度 size。 数据写入到文件后最终的格式如下 2.反序列化 // MiniPBCoder.cpp void MiniPBCoder::decodeMap(unordered_mapstring, MMBuffer dic,const MMBuffer oData,size_t size) {MiniPBCoder oCoder(oData); // 使用 MMBuffer 缓存构建 MiniPBCoderoCoder.decodeOneMap(dic, size); // 调用 decodeOneMap 方法进行反序列化 }void MiniPBCoder::decodeOneMap(unordered_mapstring, MMBuffer dic, size_t size) {if (size 0) {// 通过 CodedInputData 读取 Varint32 的 valueSize 值auto length m_inputData-readInt32();}while (!m_inputData-isAtEnd()) {// 通过 CodedInputData 读取 key 值const auto key m_inputData-readString();if (key.length() 0) {// 通过 CodedInputData 读取 value 值auto value m_inputData-readData();if (value.length() 0) {dic[key] move(value);} else { // 如果 value 值为0删除 key 对应的项dic.erase(key);}}} }相比于序列化来说反序列化的逻辑相对简单通过 CodedInputData 读取 Varint32 的 valueSize 值随后不断循环通过 CodedInputData 分别读取 key 值和 value 值。 12.文件锁 SharedPreferences 在 Android 7.0 之后便不再对跨进程模式进行支持原因是跨进程无法保证线程安全而 MMKV 则通过文件锁解决了这个问题。 其实本来是可以采用在共享内存中创建 pthread_mutex 互斥锁来实现两端线程的读写同步但由于 Android 对 Linux 的部分同步互斥机制进行了阉割使得它无法保证当持有锁的进程意外死亡时并不会释放其拥有的锁此时若多进程之间存在竞争那么阻塞的进程将不会被唤醒导致等待锁的进程饿死。 文件锁是一种用来保证多个进程对同一个文件的安全访问的机制。文件锁可以分为两种类型建议性锁和强制性锁 建议性锁是一种协作式的锁它只有在所有参与的进程都遵守锁的规则时才有效强制性锁是一种强制式的锁它由内核或文件系统来强制执行不需要进程的配合。 flock 函数是一种使用文件描述符来实现文件锁的方法。该函数的功能是对一个已打开的文件描述符 fd 进行锁定或解锁操作它的函数原型如下 #include sys/file.h int flock(int fd, int operation);函数的参数如下 fd已打开的文件描述符必须是可读或可写的不能是只执行的operation表示锁类型的整数可以是 LOCK_SH、LOCK_EX、LOCK_UN 或 LOCK_NB 的组合。 锁类型如下 LOCK_SH共享锁允许多个进程同时对文件进行读操作但不允许写操作LOCK_EX独占锁只允许一个进程对文件进行读写操作其他进程都不能访问文件LOCK_UN解锁释放之前的锁定允许其他进程访问文件LOCK_NB非阻塞如果不能立即获得锁不会等待而是返回错误。 函数的用法 打开一个文件获得一个文件描述符fd调用 flock 函数传入 fd 和想要的锁类型例如 LOCK_EX。如果成功返回 0表示获得了锁可以对文件进行读写操作。如果失败返回 -1并设置 errno表示没有获得锁可能是因为文件已经被其他进程锁定或者其他错误发生完成文件操作后调用 flock 函数传入 fd 和 LOCK_UN释放锁关闭文件。 文件锁存在着一定缺点 不支持递归加锁重入锁如果我们重复加锁会导致阻塞如果解锁会把所有的锁都给解除死锁问题如果我们两个进程同时将读锁升级为死锁可能会陷入互相等待从而发生死锁。 MMKV 采用了文件锁的设计并对文件锁的递归锁和锁升级/降级机制进行了实现。 递归锁可重入若一个进程/线程已经拥有了锁那么后续的加锁操作不会导致卡死并且解锁也不会导致外层的锁被解掉。而由于文件锁是基于状态的没有计数器因此在解锁时会导致外层的锁也被解掉锁升级/降级锁升级是指将已经持有的共享锁升级为互斥锁也就是将读锁升级为写锁锁降级则是反过来。文件锁支持锁升级但是容易死锁假如 A、B 进程都持有了读锁现在都想升级到写锁就会发生死锁。另外由于文件锁不支持递归锁也导致了锁降级一降就降到没有锁。 1.加锁 MMKV 中调用 FileLock.lock 或 FileLock.try_lock 方法进行文件加锁他们两者的区别是前者是阻塞式获取锁会等待到锁的释放后者则是非阻塞式获取锁其最终都会调用 FileLock.doLock 方法完成锁的获取 // InterProcessLock.cpp bool FileLock::lock(LockType lockType) {// 阻塞式需等待return doLock(lockType, true); }bool FileLock::try_lock(LockType lockType) {// 非阻塞式不需等待return doLock(lockType, false); }bool FileLock::doLock(LockType lockType, bool wait) {if (!isFileLockValid()) {return false; // 文件锁不可用返回 false}bool unLockFirstIfNeeded false; // 是否需要先解锁if (lockType SharedLockType) { // 如果是共享锁加读锁共享锁m_sharedLockCount; // 读锁数量// 不希望共享锁破坏任何现有的锁即有其他锁的情况下不需要真正再加一次锁if (m_sharedLockCount 1 || m_exclusiveLockCount 0) {return true;}} else {m_exclusiveLockCount;// 不希望排他锁破坏现有的排他锁即之前加过写锁则不需要再重新加锁if (m_exclusiveLockCount 1) {return true;}// 避免死锁要加写锁如果已经存在读锁可能是其他进程获取的如果是则需要先将自己的读锁释放掉再加写锁if (m_sharedLockCount 0) {unLockFirstIfNeeded true;}}// 加读锁或写锁获取到的锁类型LOCK_SH 或 LOCK_EXint realLockType LockType2FlockType(lockType);int cmd wait ? realLockType : (realLockType | LOCK_NB);if (unLockFirstIfNeeded) {// 如果已经存在读锁先看看能否获取写锁成功直接返回否者需先解读锁再加写锁auto ret flock(m_fd, realLockType | LOCK_NB);if (ret 0) {return true;}// 解除共享锁以防止死锁ret flock(m_fd, LOCK_UN);if (ret ! 0) {MMKVError(fail to try unlock first fd%d, ret%d, error:%s, m_fd, ret,strerror(errno));}}// 执行对应的加锁读锁或写锁auto ret flock(m_fd, cmd);if (ret ! 0) {MMKVError(fail to lock fd%d, ret%d, error:%s, m_fd, ret, strerror(errno));return false;} else {return true;} }通过分析可知对于写锁而言在加写锁时如果当前进程持有了读锁那我们需要尝试加写锁。如果加写锁失败说明其他线程持有了读锁则需要将目前的读锁释放掉再加写锁从而避免死锁这种情况说明两个进程的读锁都想升级为写锁。 MMKV 中通过维护 m_sharedLockCount 以及 m_exclusiveLockCount 从而实现递归加锁如果存在其他锁时就不再需要真正第二次加锁。 2.解锁 MMKV 通过 FileLock.unlock 方法完成文件解锁 // InterProcessLock.cpp bool FileLock::unlock(LockType lockType) {if (!isFileLockValid()) {return false; // 文件锁不可用返回 false}bool unlockToSharedLock false; // 是否解锁到共享锁读锁if (lockType SharedLockType) {if (m_sharedLockCount 0) { // 共享锁只是还未加锁无须解锁return false;}m_sharedLockCount--; // 解读锁只需减少 m_sharedLockCount 即可// 此时如果存在其它的锁则不需要真正的解锁if (m_sharedLockCount 0 || m_exclusiveLockCount 0) {return true;}} else {if (m_exclusiveLockCount 0) {return false; // 如果是写锁只是还未加锁无须解锁}m_exclusiveLockCount--; // 解读锁只需减少 m_exclusiveLockCount 即可if (m_exclusiveLockCount 0) {return true;}// 当所有排他锁都解锁后恢复共享锁// 如果之前存在写锁则只是降级为读锁因为之前是将读锁升级为了写锁此时只需降回即可if (m_sharedLockCount 0) {unlockToSharedLock true;}}int cmd unlockToSharedLock ? LOCK_SH : LOCK_UN;// 执行对应的操作加读锁或解锁auto ret flock(m_fd, cmd);if (ret ! 0) {MMKVError(fail to unlock fd%d, ret%d, error:%s, m_fd, ret, strerror(errno));return false;} else {return true;} }在解锁时对于解写锁时如果我们的写锁是由读锁升级而来则不会真的进行解锁而是改为加读锁从而实现将写锁降级为读锁因为读锁还没解除。 13.状态同步 MMKV 既然支持跨进程共享文件那就必然面临状态同步问题有以下几种 写指针同步其它进程可能写入了新的键值对此时需要更新写指针的位置。通过文件头部保存的有效内存大小 m_actualSize每次都对其进行比较从而实现写指针的同步。内存重整同步如果发生了内存重整可能导致前面的键值全部失效需要全部清除重新加载。内存重整同步是通过使用一个单调递增的序列号 m_sequence 来进行比较每进行一次内存重整将其值 1 从而实现。内存增长同步通过文件大小的比较从而实现。 MMKV 中的状态同步通过 checkLoadData 方法实现 void MMKV::checkLoadData() {if (m_needLoadFromFile) {SCOPEDLOCK(m_sharedProcessLock);m_needLoadFromFile false;// 需重新加载文件数据loadFromFile();return;}if (!m_isInterProcess) {return;}if (!m_metaFile.isFileValid()) {return; // 文件不可用直接返回}// TODO: atomic lock m_metaFile? 原子锁 m_metaFileMMKVMetaInfo metaInfo;metaInfo.read(m_metaFile.getMemory());if (m_metaInfo.m_sequence ! metaInfo.m_sequence) {MMKVInfo([%s] oldSeq %u, newSeq %u, m_mmapID.c_str(), m_metaInfo.m_sequence,metaInfo.m_sequence);SCOPEDLOCK(m_sharedProcessLock);// 序列号 m_sequence 不同说明发生了内存重整清除后重新加载clearMemoryState();loadFromFile();} else if (m_metaInfo.m_crcDigest ! metaInfo.m_crcDigest) { // CRC 不同说明发生了改变MMKVDebug([%s] oldCrc %u, newCrc %u, m_mmapID.c_str(), m_metaInfo.m_crcDigest,metaInfo.m_crcDigest);SCOPEDLOCK(m_sharedProcessLock);size_t fileSize 0;if (m_isAshmem) {fileSize m_size;} else {struct stat st {0};if (fstat(m_fd, st) ! -1) {fileSize (size_t) st.st_size;}}if (m_size ! fileSize) { // 如果 size 不同说明发生了文件增长MMKVInfo(file size has changed [%s] from %zu to %zu, m_mmapID.c_str(), m_size,fileSize);clearMemoryState();loadFromFile();} else {// size 相同说明需要进行写指针同步只需要部分进行 loadFilepartialLoadFromFile();}} }除了写指针同步的情况其余情况都是清除后重新读取文件实现同步。 总结 MMKV 是一个基于 mmap 实现的 K-V 存储工具它的序列化基于 Protobuf 实现并引入了 CRC 冗余校验从而对文件完整性进行校验并且它支持通过 AES 算法对 Protobuf 文件进行加密。 MMKV 的初始化过程主要完成了对存储根目录 rootDir 的初始化及创建其位于应用的内部存储 file 下的 mmkv 文件夹。MMKV 实例的获取需要通过 mmapWithID 完成结合传入的 mmapId 与 relativePath 通过 md5 生成一个唯一的 mmapKey通过它查找 map 获取对应的 MMKV 实例若找不到对应的实例会构建一个新的 MMKV 对象。Java 层通过持有 Native 层对象的地址从而实现与 Native 对象进行通信。MMKV 对象创建时会创建用于 AES 加密的 AESCrypt 对象并且会调用 loadFromFile 方法将文件的内容通过 mmap 映射到内存中映射会以页的整数倍进行若不足的地方会补 0。映射完成后会构造对应的 MMBuffer 对映射区域进行管理并创建对应的 CodedOutputData 对象之后会通过 MiniPBCoder 将其读入到 m_dic 这个 map 中它以 String 为 keyMMBuffer 为 value。MMKV 在数据写入前会调用 checkLoadData 方法确保数据已正确读入并且对跨进程的信息进行同步之后会将数据转换为 MMBuffer 对象并写入 map 中 然后调用 ensureMemorySize 在确保映射空间足够的情况下通过构造 MMKV 对象时创建的 CodedOutputData 将数据写入 Protobuf 文件。并且 MMKV 的数据更新和写入都是通过在文件后进行 append会造成存在冗余 key-value 数据。ensureMemorySize 方法在内存不足的情况下首先进行内存重整它会清空文件从 map 重新将数据写入文件从而清理冗余数据如果仍然不够则会以每次两倍对文件大小进行扩容并重新通过 mmap 进行映射。MMKV 的删除操作实际上是通过在文件中对同样的 key 写入长度为 0 的 MMBuffer 实现当读取时发现其长度为 0则将其视为已删除。MMKV 的读取是通过 CodedInputData 实现它在读取的 MMBuffer 长度为 0 时会将其视为不存在实际上 CodedInputData 与 CodedOutputData 就是与 MMBuffer 进行交互的桥梁。MMKV 还存在着文件回写机制在以下的时机会将 map 中的数据立即写入文件 通过 MMKV.reKey 方法修改加密的 key通过 MMKV.removeValuesForKeys 删除一系列 key读取文件时文件校验或 CRC 冗余校验失败。 MMKV 支持跨进程读写通过文件锁实现跨进程加锁并且通过对文件锁引入读锁和写锁的计数从而解决了其存在的不支持递归锁和锁升级/降级问题。不使用 pthread_mutex 通过共享内存加锁的原因是 Android 对 Linux 进行了阉割如果持有锁的进程被杀死无法保证清除锁的信息可能导致等待锁的其他进程饿死。MMKV 解决了写指针同步、内存重整同步以及内存增长同步 等问题写指针同步通过在文件的起始处添加一个写指针值在 checkLoadData 中会对它进行比较从而获取最新的写指针 m_actualSize内存重整同步通过一个序号 m_sequence 来实现每当发生一次内存重整对其 1通过比较值即可确定内存增长同步则通过比较文件大小实现。 MMKV使用时的注意事项: 保证每一个文件存储的数据都比较小也就说需要把数据根据业务线存储分散避免虚拟内存消耗过快适当的时候释放一部分内存数据比如在 App 中监听 onTrimMemory 方法在 Java 内存吃紧的情况下进行 MMKV 的 trim 操作不需要使用的时候最好把 MMKV 给 close 掉甚至调用 onExit 方法退出。 参考文献 github.com/Tencent/MMKV
http://www.dnsts.com.cn/news/43756.html

相关文章:

  • 东营网站seo外包菏泽网站建设网站
  • 阿里云网站部署主机租赁平台
  • 完全自建网站在线p图修改文字
  • 越秀网站建设哪家好mysql 网站登录密码
  • 动漫做的游戏 迅雷下载网站有哪些昆明响应式网站
  • 酒店网站 方案怎么把网站做二维码
  • 卡盟网站怎么做湖南网站推
  • 如何做国外外贸网站展架立式落地式
  • 网站开发主要使用的技术ps怎么做网站设计
  • 加强企业网站建设作用适合用于网站开发的工具
  • 网站的开发环境设计网站制作案例怎么样
  • 做网站带来的好处自助免费建网站
  • 快速判断网站开发语言网站制作公司哪家好
  • 高端网站制作的公司广州各区最新动态
  • 重庆市企业网站建设张家港高端网站制作
  • 广州宝安建网站机关网站建设制度
  • 做网站需要会什么 知乎网站建设的收获体会
  • 企业做外贸网站常用术语设计工作室怎么起步
  • 查询网站备案查询vs设置网站开发环境
  • 网站想做个链接怎么做的百度公司有哪些部门
  • 济南正规网站建设公司wordpress 博客园
  • 网站建设 seo模块遵义新蓝外国语学校网站建设
  • 纪检网站建设方案网页版梦幻西游探案任务攻略
  • 阿里云心选建站wordpress 新手教程
  • wordpress theme是什么百度百科优化
  • 广东省自然资源厅领导分工徐州网站优化价格
  • 网站seo查询站长之家建筑模板多少钱一张什么尺寸
  • 网站建设标题wamp在网站建设中的功能及协作关系
  • 宿迁住房和城乡建设部网站百度搜索推广怎么做
  • 网站建设与管理论文金坛市建设局网站