大家好,我是学徒小z,近期学习了一些关于鸿蒙第三方库的知识,今天分享给大家
文章目录
- 一、引言
- 二、核心技术
- 三、类
-
- [1. 模板类:ScopedLock:](#1. 模板类:ScopedLock:)
- [2. ThreadLock](#2. ThreadLock)
- [3. MMBuffer](#3. MMBuffer)
- [4. MiniPBCoder](#4. MiniPBCoder)
- [5. MMKV](#5. MMKV)
- PBUtility
学习源码的小知识:可以将第三方库源码下载到本地运行,然后断点调试,一步一步研究运行过程
一、引言
MMKV 基于c++,根据腾讯自研的"core" 模块实现核心逻辑,主要负责以下功能:
- 内存映射(Memory Mapping) :通过操作系统的 mmap 系统调用,将文件直接映射到内存中,从而实现高效的读写操作。
- 序列化与反序列化 :使用 Google 的 Protocol Buffers(protobuf)进行数据的序列化和反序列化,确保数据存储的高效性和兼容性
- 线程安全 :通过锁机制(如互斥锁)保证多线程环境下的数据一致性。
使用POSIX线程库中的pthread_once来进行初始化,确保在多个线程中也只执行一次
二、核心技术
Mmap
mmap 是一种系统调用,用于将文件或设备的内容映射到进程的虚拟地址空间中,通过 mmap,文件的数据可以直接被映射到内存中,从而允许应用程序像操作内存一样操作文件内容。
核心机制
- 用户空间与内核空间的关系
- 用户空间 :应用程序运行的地方,无法直接访问物理内存。
- 内核空间 :操作系统内核管理的地方,负责处理硬件资源(如物理内存、磁盘等)。
- 虚拟内存 :每个进程都有自己的虚拟地址空间,这些虚拟地址通过页表映射到物理地址。
mmap 的作用是将文件或设备映射到进程的虚拟地址空间中,而不是直接映射到物理内存。具体过程如下: - 调用 mmap 后,操作系统会在进程的虚拟地址空间中分配一段区域。
- 这段虚拟地址区域通过页表映射到内核管理的物理内存或文件内容。
- 当用户访问这段虚拟地址时,操作系统会根据需要将文件内容加载到物理内存中(按需分页)
mmap的核心原理(硬件到软件的全链路)
- 硬件层面:虚拟内存与MMU的协作
- 页表映射机制: mmap依赖CPU的内存管理单元(MMU)实现虚拟地址到物理地址的动态映射。当进程调用mmap()时,操作系统在页表中创建映射条目,但此时物理内存并未分配。
- 缺页中断处理: 首次访问映射区域时触发缺页中断(Page Fault),MMU将文件对应数据块从磁盘加载到物理内存,并建立完整的页表映射。该过程对应用透明,实现按需加载(Demand Paging)。
- 操作系统层:内核态与用户态的交互
- 直接内存映射: 内核通过struct vmareastruct结构管理映射区域,将文件偏移量(pgoff)与虚拟地址关联。数据修改通过脏页回写机制(Dirty Page Tracking)异步同步到磁盘)。
- 零拷贝实现: 文件I/O绕过内核缓冲区(Page Cache),用户空间直接操作映射内存,消除传统read()/write()的两次拷贝(用户↔内核↔磁盘)。
- 文件系统层:内存与磁盘的同步
- 写时复制(COW)优化: 对私有映射(MAP_PRIVATE)的修改仅影响进程私有副本,原始文件不受影响,节省物理内存。
- msync强制同步: 调用msync(addr, len, MS_SYNC)可强制将指定内存区域刷盘,确保数据持久性。
优势
- 减少CPU拷贝:传统I/O需4次数据搬运(磁盘→内核→用户→内核→磁盘),mmap仅需2次(磁盘→内存→磁盘),消除用户态与内核态间的冗余拷贝。
- 降低上下文切换:read()/write()每次调用涉及2次上下文切换(用户态↔内核态)
mmap仅在初始映射和缺页时触发切换。
三、类
1. 模板类:ScopedLock:
通过宏定义和 RAII(Resource Acquisition Is Initialization)机制实现一个线程安全的锁管理工具
功能
- 自动加锁与解锁 :
- 使用 mmkv::ScopedLock 类模板,在构造函数中加锁,在析构函数中解锁。
- 确保即使发生异常,锁也能被正确释放。
- 支持指针和非指针类型的锁 :
- 通过 std::remove_pointer 提取锁的实际类型,兼容指针(如 std::mutex*)和非指针(如 std::mutex)类型的锁。
- 避免变量名冲突 :
- 使用编译器内置宏 COUNTER 生成唯一的变量名,防止多个锁变量名冲突。
- 禁止拷贝与赋值 :
- 删除拷贝构造函数和赋值操作符,防止误用导致资源管理问题。
2. ThreadLock
提供线程同步相关的功能。根据不同的编译条件(是否定义了 MMKV_USING_PTHREAD),ThreadLock 类会采用不同的底层实现方式,一种是基于 pthread 的实现,另一种是基于 Windows 临界区(CRITICAL_SECTION)的实现。
为什么提供两种方式?不同的操作系统提供了不同的线程同步原语。pthread是 POSIX 线程库,它提供了一套在类 Unix 系统(如 Linux、macOS 等)上进行线程管理和同步的标准接口。而 Windows 操作系统有自己的一套线程同步机制,其中临界区(CRITICAL_SECTION)是一种常用的线程同步原语。
实现
-
构造函数
重入:在多线程环境下,同一线程可多次获取同一把锁。假设线程 A 已获取可重入锁,执行函数 f () 过程中,f () 内部又调用需要该锁的函数 g () ,因锁可重入,线程 A 能再次获取锁进入 g () 执行,且不会死锁,执行完 g () 后,线程 A 能正确释放多次获取的锁,保证程序正常运行。
cppThreadLock::ThreadLock() : m_lock({}) { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); //设置可重入锁,允许同一线程重复获取资源 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&m_lock, &attr); pthread_mutexattr_destroy(&attr); }
-
删除拷贝构造函数和拷贝赋值运算符,这样做的主要目的是为了避免ThreadLock对象被意外地拷贝。假如允许拷贝,可能会出现多个ThreadLock对象指向同一个锁资源。例如,一个对象已经对锁进行了锁定操作,另一个对象可能会在不知情的情况下试图对同一个锁进行操作,导致死锁或其他不可预期的行为。
MemoryFile
MemoryFile 类是在 MMKV 项目中用于管理内存映射文件的类
成员变量
-
File m_diskFile:一个 File 类的对象,用于管理磁盘文件的相关操作,如打开、关闭等。
-
HANDLE m_fileMapping(仅在 MMKV_WIN32 定义):在 Windows 平台下,用于表示文件映射对象的句柄。
-
void *m_ptr:指向内存映射区域的指针,通过该指针可以访问文件的内容。
-
size_t m_size:内存映射区域的大小,也就是文件的大小。
-
const bool m_readOnly:表示文件是否以只读模式打开。
-
const FileType m_fileType(仅在 MMKV_ANDROID 定义):在 Android 平台下,用于表示文件的类型(普通文件或 Ashmem 文件)。
私有成员函数
-
Mmap
MemoryFile类的mmap方法用于将文件映射到内存中,不同平台下有不同的实现。
主要步骤
- 保存旧的指针。
- 错误处理与恢复
在某些情况下,如果内存映射操作失败,可能需要恢复到之前的状态。 - 复用原有映射区域(部分场景)
在某些情况下,如果之前的内存映射区域不再使用,但地址空间仍然保留,传递旧的 m_ptr 可以尝试复用这个地址空间。例如,当之前的映射被 munmap 解除映射后,再次调用 mmap 并传递之前的地址,有可能会重新利用这块地址空间进行新的映射,从而避免频繁的地址空间分配和管理开销。 - 根据m_readOnly属性确定保护模式。
- 调用::mmap进行内存映射。
- 若映射失败,记录错误信息,恢复指针并返回false。
- 记录映射成功的信息,返回true。
cpp
bool MemoryFile::mmap() {
auto oldPtr = m_ptr;
auto mode = m_readOnly ? PROT_READ : (PROT_READ | PROT_WRITE);
m_ptr = (char *) ::mmap(m_ptr, m_size, mode, MAP_SHARED, m_diskFile.m_fd, 0);
if (m_ptr == MAP_FAILED) {
MMKVError("fail to mmap [%s], mode %x, %s", m_diskFile.m_path.c_str(), mode, strerror(errno));
m_ptr = nullptr;
return false;
}
MMKVInfo("mmap to address [%p], oldPtr [%p], [%s]", m_ptr, oldPtr, m_diskFile.m_path.c_str());
return true;
}
公有成员函数
- size_t getFileSize() const:返回内存映射区域的大小,即文件的大小。
- size_t getActualFileSize() const:返回磁盘上文件的实际大小,通过调用 m_diskFile.getActualFileSize() 实现。
- void *getMemory():返回指向内存映射区域的指针,通过该指针可以访问文件的内容。
- const MMKVPath_t &getPath():返回文件的路径,通过调用 m_diskFile.getPath() 实现。
- MMKVFileHandle_t getFd():返回文件描述符,通过调用 m_diskFile.getFd() 实现。
- bool truncate(size_t size):截断文件到指定的大小,新扩展的文件内容将被清零。
- bool msync(SyncFlag syncFlag):将内存中的数据同步到磁盘上。
- void reloadFromFile(size_t expectedCapacity = 0):重新从文件加载数据到内存映射区域。
- void clearMemoryCache():调用 doCleanMemoryCache(false) 来清理内存映射缓存。
- bool isFileValid():检查文件是否有效,不同平台下有不同的实现。
3. MMBuffer

是专为高效内存管理设计的 智能缓冲区容器,用于高效处理不同大小的数据存储,减少内存分配和复制的次数。特别是对于小数据,直接使用栈内存,可以提升性能,而大数据则使用堆内存,避免栈溢出,之后仍然会写入磁盘。
内存管理方式
MMBuffer类通过一个枚举MMBufferType来区分不同的内存管理方式:
- MMBufferType_Small:对于较小的缓冲区,数据存储在栈内存中。栈内存的分配和释放由系统自动处理,无需手动管理,适用于小数据量的场景,可提高效率。
- MMBufferType_Normal:对于较大的缓冲区,数据存储在堆内存中。堆内存需要手动分配(使用malloc)和释放(使用free),适用于大数据量的场景。
数据存储结构
- 当使用堆内存(MMBufferType_Normal)时,存储isNoCopy(是否复制标志)、size(缓冲区大小)、ptr(指向堆内存的指针),在苹果平台(MMKV_APPLE定义时)还会存储NSData *m_data。
- 当使用栈内存(MMBufferType_Small)时,存储paddedSize(填充大小)和paddedBuffer(一个长度为 10 的数组,用于存储数据)。
4. MiniPBCoder
是一个用于处理协议缓冲区(Protocol Buffers,简称 PB)编码和解码的类,在这个特定的代码实现中,它主要用于对不同类型的数据进行编码和解析,以实现数据的序列化和反序列化。
数据成员管理
-
m_inputBuffer:指向输入数据的缓冲区。
-
m_inputData 和 m_inputDataDecrpt:用于读取输入数据,其中 m_inputDataDecrpt 可能用于处理加密的数据。
-
m_outputBuffer 和 m_outputData:用于存储和写入输出数据。
-
m_encodeItems:存储编码项的向量,用于管理编码过程中的各种信息。
cppconst MMBuffer *m_inputBuffer = nullptr; CodedInputData *m_inputData = nullptr; CodedInputDataCrypt *m_inputDataDecrpt = nullptr; MMBuffer *m_outputBuffer = nullptr; CodedOutputData *m_outputData = nullptr; std::vector<PBEncodeItem> *m_encodeItems = nullptr;
编码相关功能
- 准备编码功能
- 提供了多个 prepareObjectForEncode 函数的重载,用于准备不同类型的数据进行编码 。这些函数会根据输入数据的类型,将数据转换为合适的编码项,并添加到 m_encodeItems 中。
cpp
size_t prepareObjectForEncode(const MMKVVector &vec);
size_t prepareObjectForEncode(const MMBuffer &buffer);
#ifndef MMKV_APPLE
size_t prepareObjectForEncode(const std::string &str);
size_t prepareObjectForEncode(const MMKV_STRING_CONTAINER &vector);
#ifdef MMKV_HAS_CPP20
size_t prepareObjectForEncode(const std::span<const int32_t> &vec);
size_t prepareObjectForEncode(const std::span<const uint32_t> &vec);
size_t prepareObjectForEncode(const std::span<const int64_t> &vec);
size_t prepareObjectForEncode(const std::span<const uint64_t> &vec);
#endif // MMKV_HAS_CPP20
#else
size_t prepareObjectForEncode(__unsafe_unretained NSObject *obj);
#endif // MMKV_APPLE
- 写入编码数据
- 编码数据获取
解码相关功能
5. MMKV
属性
1. 与映射文件和ID相关的属性
std::string m_mmapKey;
std::string m_mmapID;
- 作用:
- m_mmapKey:可能用于标识映射文件的键,在内存映射相关操作中用于唯一标识该MMKV实例对应的内存映射区域,比如在不同的MMKV实例之间进行区分。
- m_mmapID:用于唯一标识MMKV实例,可由用户自定义,例如不同业务模块可以使用不同的mmapID来创建各自独立的MMKV实例。
模式相关属性
const MMKVMode m_mode;
- 作用:存储MMKV的工作模式,MMKVMode是一个枚举类型,包含如单进程模式(MMKV_SINGLE_PROCESS)、多进程模式(MMKV_MULTI_PROCESS)等不同模式,根据这个模式MMKV会进行不同的处理,例如在多进程模式下会进行额外的同步操作以保证数据一致性。
2. 路径相关属性
MMKVPath_t m_path;
MMKVPath_t m_crcPath;
- 作用:
- m_path:存储MMKV数据文件的路径,MMKV将数据存储在文件中,这个路径指定了数据文件所在的位置。
- m_crcPath:存储用于存储CRC(循环冗余校验)信息的文件路径,CRC用于数据校验,确保数据的完整性。
数据存储相关属性
mmkv::MMKVMap *m_dic;
mmkv::MMKVMapCrypt *m_dicCrypt;
- 作用:
- m_dic:指向MMKVMap对象的指针,用于存储非加密的数据,可能是一个键值对存储结构,用于快速查找和存储数据。
- m_dicCrypt:指向MMKVMapCrypt对象的指针,用于存储加密的数据,当MMKV启用加密功能时,会使用这个对象来处理加密数据的存储和读取。
3. 容量相关属性
size_t m_expectedCapacity;
- 作用:表示MMKV预期的容量大小,在初始化时可以指定这个值,MMKV会根据这个预期容量来分配相应的内存空间,避免频繁的内存重新分配。
文件和数据大小相关属性
mmkv::MemoryFile *m_file;
size_t m_actualSize;
mmkv::CodedOutputData *m_output; - 作用:
- m_file:指向MemoryFile对象的指针,用于操作MMKV的数据文件,进行文件的读写操作。
- m_actualSize:存储MMKV数据文件的实际大小,即文件中存储的数据的实际字节数。
- m_output:指向CodedOutputData对象的指针,用于编码和输出数据,将数据写入文件时使用。
加载和写回标志属性
bool m_needLoadFromFile;
bool m_hasFullWriteback;
- 作用:
- m_needLoadFromFile:表示是否需要从文件中加载数据,在某些情况下,例如MMKV初始化时,可能需要从文件中读取已存储的数据,这个标志用于控制是否执行加载操作。
- m_hasFullWriteback:表示是否已经进行了完整的写回操作,写回操作是将内存中的数据同步到文件中,这个标志用于记录写回操作的状态。
4. CRC和元信息相关属性
uint32_t m_crcDigest;
mmkv::MemoryFile *m_metaFile;
mmkv::MMKVMetaInfo *m_metaInfo;
- 作用:
- m_crcDigest:存储数据的CRC校验值,用于验证数据的完整性,在数据读取和写入时会进行CRC计算和校验。
- m_metaFile:指向MemoryFile对象的指针,用于操作MMKV的元信息文件,元信息文件存储了一些关于MMKV的额外信息,如版本号、数据结构信息等。
- m_metaInfo:指向MMKVMetaInfo对象的指针,用于存储和操作MMKV的元信息,包含了MMKV的一些元数据。
加密相关属性
mmkv::AESCrypt *m_crypter;
- 作用:指向AESCrypt对象的指针,用于进行AES加密和解密操作,当MMKV启用加密功能时,会使用这个对象来对数据进行加密和解密。
5. 锁相关属性
mmkv::ThreadLock *m_lock;
mmkv::FileLock *m_fileLock;
mmkv::InterProcessLock *m_sharedProcessLock;
mmkv::InterProcessLock *m_exclusiveProcessLock;
- 作用:
- m_lock:指向ThreadLock对象的指针,用于线程级别的同步,保证在多线程环境下对MMKV的操作是线程安全的。
- m_fileLock:指向FileLock对象的指针,用于文件级别的锁定,防止多个进程同时对同一个文件进行操作。
- m_sharedProcessLock:指向InterProcessLock对象的指针,用于进程间的共享锁定,允许多个进程同时对某些资源进行只读操作。
- m_exclusiveProcessLock:指向InterProcessLock对象的指针,用于进程间的排他锁定,当一个进程对某个资源进行写操作时,其他进程不能同时对该资源进行读写操作。
6. 过期和比较设置相关属性
bool m_enableKeyExpire = false;
uint32_t m_expiredInSeconds = ExpireNever;
bool m_enableCompareBeforeSet = false;
- 作用:
- m_enableKeyExpire:表示是否启用键过期功能,默认值为false,当设置为true时,MMKV会根据m_expiredInSeconds的值来判断键是否过期。
- m_expiredInSeconds:存储键的过期时间,单位为秒,ExpireNever表示键永不过期。
- m_enableCompareBeforeSet:表示是否在设置值之前进行比较,用于某些需要在更新值之前检查值是否发生变化的场景。
7. 平台相关属性(仅在MMKV_APPLE定义时)
#ifdef MMKV_APPLE
using MMKVKey_t = NSString *__unsafe_unretained;
static bool isKeyEmpty(MMKVKey_t key) { return key.length <= 0; }
#define mmkv_key_length(key) key.length
- 作用:
- MMKVKey_t:在苹果平台上,定义MMKVKey_t为NSString *__unsafe_unretained类型,用于表示键的类型。
- isKeyEmpty:静态函数,用于判断键是否为空,在苹果平台上通过判断NSString的长度是否小于等于0来判断。
- mmkv_key_length:宏定义,用于获取键的长度,在苹果平台上直接返回NSString的长度。
成员函数
- void loadFromFile();
- 功能:从文件中加载数据
- void partialLoadFromFile();
- 功能:从文件中部分加载数据,可能是只加载部分数据块或者某些特定的数据,
- void loadMetaInfoAndCheck();
- 功能:加载元信息并进行检查,元信息可能包含文件的一些头部信息、版本信息等,加载后会对这些信息进行有效性检查。
- void checkDataValid(bool &loadFromFile, bool &needFullWriteback);
- 功能:检查数据的有效性,并通过引用参数 loadFromFile 和 needFullWriteback 来返回检查结果。loadFromFile 可能表示是否需要从文件重新加载数据,needFullWriteback 可能表示是否需要将数据全部写回文件。
- void checkLoadData();
- 功能:检查已加载的数据,可能是检查数据的完整性、一致性等。
- bool isFileValid();
- 功能:判断文件是否有效,返回一个布尔值。如果文件有效则返回 true,否则返回 false。
- bool checkFileCRCValid(size_t actualSize, uint32_t crcDigest);
- 功能:检查文件的 CRC(循环冗余校验)是否有效,actualSize 表示文件的实际大小,crcDigest 表示文件的 CRC 校验值。
- void recalculateCRCDigestWithIV(const void *iv);
- 功能:使用初始化向量(IV)重新计算文件的 CRC 校验值,iv 是一个指向初始化向量的指针。
- void recalculateCRCDigestOnly();
- 功能:仅重新计算文件的 CRC 校验值,不使用初始化向量。
- void updateCRCDigest(const uint8_t *ptr, size_t length);
- 功能:更新文件的 CRC 校验值,ptr 是一个指向需要更新数据的指针,length 表示数据的长度。
- size_t readActualSize();
- 功能:读取文件的实际大小,并返回该大小。
- void oldStyleWriteActualSize(size_t actualSize);
- 功能:以旧的方式将文件的实际大小写入文件,actualSize 表示需要写入的实际大小。
PBUtility
主要作用是提供一组工具函数和常量,用于处理 Protobuf 编码和解码过程中涉及到的数据类型转换和大小计算。这些工具函数确保了 MMKV 在跨平台和多语言环境中能够正确地处理和传输数据,特别是在涉及二进制数据和网络通信的场景中。