鸿蒙第三方库MMKV源码学习笔记

大家好,我是学徒小z,近期学习了一些关于鸿蒙第三方库的知识,今天分享给大家

文章目录

学习源码的小知识:可以将第三方库源码下载到本地运行,然后断点调试,一步一步研究运行过程

一、引言

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 能正确释放多次获取的锁,保证程序正常运行。

    cpp 复制代码
    ThreadLock::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:存储编码项的向量,用于管理编码过程中的各种信息。

    cpp 复制代码
    const 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 在跨平台和多语言环境中能够正确地处理和传输数据,特别是在涉及二进制数据和网络通信的场景中。

相关推荐
E___V___E31 分钟前
MySQL数据库入门到大蛇尚硅谷宋红康老师笔记 高级篇 part 2
数据库·笔记·mysql
啄缘之间2 小时前
4.6 学习UVM中的“report_phase“,将其应用到具体案例分为几步?
学习·verilog·uvm·sv
Huang兄2 小时前
鸿蒙-状态管理V1
华为·harmonyos
爱学习的小王!4 小时前
nvm安装、管理node多版本以及配置环境变量【保姆级教程】
经验分享·笔记·node.js·vue
陈志化4 小时前
JMeter----笔记
笔记·jmeter
viperrrrrrrrrr74 小时前
大数据学习(49) - Flink按键分区状态(Keyed State)
大数据·学习·flink
HollowKnightZ4 小时前
论文阅读笔记:Gated CRF Loss for Weakly Supervised Semantic Image Segmentation
论文阅读·笔记
red_redemption4 小时前
自由学习记录(36)
学习
别说我什么都不会5 小时前
鸿蒙轻内核M核源码分析系列十一 (2)信号量Semaphore
操作系统·harmonyos