MMKV源码解读与理解

概述

通过 mmap 技术实现的高性能通用 key-value 组件。同时选用 protobuf 协议,进一步压缩数据存储。

标准 protobuf 不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力:将增量 kv 对象序列化后,直接 append 到内存末尾;这样同一个 key 会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的。

使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。我们需要在性能和空间上做个折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。

文件数据结构

一个 MMKV 对象会生成两个文件,一个存储数据的主文件,一个 crc 校验文件,文件名规则为:

// 主文件名为 mmapedKVKey() 返回值, crc 校验文件名为 mmapedKVKey()返回值加上 .crc 后缀
string mmapedKVKey(const string &mmapID, const MMKVPath_t *rootPath) {  if (rootPath && g_rootDir != (*rootPath)) {  return md5(*rootPath + MMKV_PATH_SLASH + string2MMKVPath_t(mmapID));  }  return mmapID;  
}

主文件

前四个字节记录了存储数据的总大小,紧接着保存每一个 key-value 对,由于使用了 protobuf 编码,为了便于读取 key、value 的数据,在保存具体数据前都先记录下其占用的字节数。由于 keyLength 和 valueLength 都为 int32 整数,因此直接按照 protobuf 编码规则读取即可,无需像 key、value 需要一个长度来确定值的结束边界。

+--------------+------------+------+--------------+--------+------------+------+---------------+-------+
| 存储的数据大小 | keyLength1 | key1 | valueLength1 | value1 | keyLength2 | key2 | valueLength2  | value2 |
+--------------+------------+------+--------------+--------+------------+------+---------------+-------+

CRC文件

CRC文件中保存的内容为以下结构体定义的数据结构,包括 crc32 校验和的值以及一堆辅助数据,用以验证文件的一致性。

struct MMKVMetaInfo {uint32_t m_crcDigest = 0;uint32_t m_version = MMKVVersionSequence;uint32_t m_sequence = 0; // full write-back countuint8_t m_vector[AES_KEY_LEN] = {};uint32_t m_actualSize = 0;// confirmed info: it's been synced to filestruct {uint32_t lastActualSize = 0;uint32_t lastCRCDigest = 0;uint32_t _reserved[16] = {};} m_lastConfirmedMetaInfo;
}

数据初始化

MMKV 对象构造时会调用 loadFromFile 读取数据,将文件中的 key-value 对读取到一个 dict 中保存。dict 是一个 std::unordered_map<std::string, mmkv::KeyValueHolder> 结构,dict 的 key 即为保存的 key-value 对中的 key。并且通过 KeyValueHolder 来保存 key-value 对的内容。

// MiniPBCoder.cpp#decodeOneMap
auto block = [position, this](MMKVMap &dictionary) {if (position) {m_inputData->seek(position);} else {m_inputData->readInt32();}while (!m_inputData->isAtEnd()) {KeyValueHolder kvHolder;// 读取 key,保存 key 的 起始位置和size信息到 KeyValueHoder 中const auto &key = m_inputData->readString(kvHolder);if (key.length() > 0) {// 读取 value,保存 value 的size信息到 KeyValueHolder,此时并不会将 value 解码出来m_inputData->readData(kvHolder);if (kvHolder.valueSize > 0) {dictionary[key] = move(kvHolder);} else {auto itr = dictionary.find(key);if (itr != dictionary.end()) {dictionary.erase(itr);}}}}
};// CodedInputData.cpp#readString
// 读取 key
string CodedInputData::readString(KeyValueHolder &kvHolder) {  kvHolder.offset = static_cast<uint32_t>(m_position);  int32_t size = this->readRawVarint32();  if (size < 0) {  throw length_error("InvalidProtocolBuffer negativeSize");  }  auto s_size = static_cast<size_t>(size);  if (s_size <= m_size - m_position) {  kvHolder.keySize = static_cast<uint16_t>(s_size);  auto ptr = m_ptr + m_position;  string result((char *) (m_ptr + m_position), s_size);  m_position += s_size;  return result;  } else {  throw out_of_range("InvalidProtocolBuffer truncatedMessage");  }  
}// CodedInputData.cpp#readData
// 读取 value
void CodedInputData::readData(KeyValueHolder &kvHolder) {  int32_t size = this->readRawVarint32();  if (size < 0) {  throw length_error("InvalidProtocolBuffer negativeSize");  }  auto s_size = static_cast<size_t>(size);  if (s_size <= m_size - m_position) {  kvHolder.computedKVSize = static_cast<uint16_t>(m_position - kvHolder.offset);  kvHolder.valueSize = static_cast<uint32_t>(s_size);  m_position += s_size;  } else {  throw out_of_range("InvalidProtocolBuffer truncatedMessage");  }  
}

数据写入与读取

这里仅分析在 Android 平台的主流程逻辑,因此对于加密功能和在 iOS 设备上的逻辑不去关注。由于 MMKV 对于 value 支持多种类型格式,这里也主要通过类型为 int 和 string 的写入和读取逻辑来进行了解。

MMBuffer

MMKV 中定义的内存单元,用来更方便的进行一些操作而抽象的结构。对于占用内存小的数据,直接保存在栈中,而对于占用内存大的数据则保存在堆中。 判断占用内存的大小取决于 sizeof(MMBuffer) - offsetof(MMBuffer, paddedBuffer) 计算的值,其实也就是 paddedBuffer[10] 的大小。这里应该是考虑到对于基本数值类型进行 protobuf 编码后最多占用10个字节,因此使用这种方式来更高效的进行内存操作。 MMBuffer 中包含一个联合体,其中的两个结构体共用存储空间,在实际使用时只能使用其中的一个。在默认情况下,编译器会对 MMBuffer 进行内存对齐,添加了 7 个填充字节,以保证 size 和 ptr 成员都按照 8 字节对齐。而对于第二个结构体,由于其成员都是 1 字节大小,因此没有进行内存对齐,没有填充字节。其内存布局如下:

+--------------------+------------------------+---------------+--------------+
|  isNoCopy(1 byte)  |    padding(7 bytes)    | size(8 bytes) | ptr(8 bytes) |
+--------------------+------------------------+---------------+--------------+
+--------------------+----------------------------+
| paddedSize(1 byte) |   paddedBuffer(10 bytes)   |
+--------------------+----------------------------+

class MMBuffer {enum MMBufferType : uint8_t {MMBufferType_Small,  // store small buffer in stack memoryMMBufferType_Normal, // store in heap memory};MMBufferType type;union {struct {MMBufferCopyFlag isNoCopy;size_t size;void *ptr;};struct {uint8_t paddedSize;// make at least 10 bytes to hold all primitive types (negative int32, int64, double etc) on 32 bit device// on 64 bit device it's guaranteed larger than 10 bytesuint8_t paddedBuffer[10];};};static constexpr size_t SmallBufferSize() {return sizeof(MMBuffer) - offsetof(MMBuffer, paddedBuffer);}public:explicit MMBuffer(size_t length = 0);MMBuffer(void *source, size_t length, MMBufferCopyFlag flag = MMBufferCopy);MMBuffer(MMBuffer &&other) noexcept;~MMBuffer();bool isStoredOnStack() const { return (type == MMBufferType_Small); }void *getPtr() const { return isStoredOnStack() ? (void *) paddedBuffer : ptr; }size_t length() const { return isStoredOnStack() ? paddedSize : size; }
};

int类型数据写入

写入的 value 为 int 类型时,计算 value 通过 protobuf 编码需要占用多少个字节,并将其编码后的结果写入到分配的内存段中。

// MMKV.cpp#set
bool MMKV::set(int32_t value, MMKVKey_t key) {  if (isKeyEmpty(key)) {  return false;  }  // 根据 protobuf 编码规则,获取 value 通过 protobuf 编码需要占用几个字节size_t size = pbInt32Size(value);  // 声明 MMBuffer,其为 MMKV 中定义的内存单元,存储了映射的指针和大小MMBuffer data(size);  // 将 MMBuffer 的 ptr 与 CodedOutputData 关联在一起,// 则 CodedOutputData 写入数据后,通过 MMBuffer 也能获取得到CodedOutputData output(data.getPtr(), size); // CodedOutputData 主要负责 protobuf 的编码逻辑,output.writeInt32(value);  return setDataForKey(move(data), key);  
}

setDataForKey

对 value 进行 protobuf 编码后,将数据写入到文件尾部,同时还需要更新 dic 中的内容,以便为后续快速读取数据服务。 查找 dic 中是否已存在要写入 key 相关的 key-value 对。

  • 当 dic 中存在这个 key,直接使用 dic 中保存的 KeyValueHolder 使用。在 doAppendDataWithKey 流程将 key 写入文件时复制 KeyValueHolder 指向的 key 数据块。这个分支走向决定了 doAppendDataWithKeyisKeyEncoded 为 true。
  • 当 dic 中没有这个 key 时, doAppendDataWithKeyisKeyEncoded 为 false,在写入文件时需要写入 keyLength,再写入 key。
// MMKV_IO.cpp#setDataForKey
auto itr = m_dic->find(key);
// 
if (itr != m_dic->end()) {  auto ret = appendDataWithKey(data, itr->second, isDataHolder);  if (!ret.first) {  return false;  }  itr->second = std::move(ret.second);  
} else {  auto ret = appendDataWithKey(data, key, isDataHolder);  if (!ret.first) {  return false;  }  m_dic->emplace(key, std::move(ret.second));  
}

appendDataWithKey

根据 setDataForKey 的逻辑分支,appendDataWithKey 也有两种逻辑,主要区别在于构造 key 的 MMBuffer 方式不一样。

  • 当 dic 中存有相关 key,对应的 MMBuffer 将 protobuf 编码的 keyLength 计算在内
  • 当 dic 中没有相关 key,对应的 MMBuffer 长度即为 key 的长度大小
// MMKV_IO.cpp#appendDataWithKey// dic 中已有相关 key 的逻辑分支
KVHolderRet_t MMKV::appendDataWithKey(const MMBuffer &data, const KeyValueHolder &kvHolder, bool isDataHolder) {  SCOPED_LOCK(m_exclusiveProcessLock);  uint32_t keyLength = kvHolder.keySize;  // size needed to encode the key  size_t rawKeySize = keyLength + pbRawVarint32Size(keyLength);  // // ensureMemorySize() might change kvHolder.offset, so have to do it early  {  auto valueLength = static_cast<uint32_t>(data.length());  if (isDataHolder) {  valueLength += pbRawVarint32Size(valueLength);  }  auto size = rawKeySize + valueLength + pbRawVarint32Size(valueLength);  // ensureMemorySize 确保有足够的空间大小以供这次写入,内部逻辑比较复杂,// 这里简单记住当申请的 mmap 空间不够时会尝试扩容bool hasEnoughSize = ensureMemorySize(size);  if (!hasEnoughSize) {  return make_pair(false, KeyValueHolder());  }  }    auto basePtr = (uint8_t *) m_file->getMemory() + Fixed32Size;  MMBuffer keyData(basePtr + kvHolder.offset, rawKeySize, MMBufferNoCopy);  return doAppendDataWithKey(data, keyData, isDataHolder, keyLength);  
}// dic 中没有相关 key 的逻辑分支
KVHolderRet_t MMKV::appendDataWithKey(const MMBuffer &data, MMKVKey_t key, bool isDataHolder) {auto keyData = MMBuffer((void *) key.data(), key.size(), MMBufferNoCopy);return doAppendDataWithKey(data, keyData, isDataHolder, static_cast<uint32_t>(keyData.length()));
}

doAppendDataWithKey

实际将 key-value 对进行写入的地方。这里需要先了解两个字段代表的含义,否则对于写入流程可能并不会太过清晰。

isDataHolder

isDataHolder 的取值从 setDataForKey 一路传下来,这里看下其函数定义,对于 isDataHolder 默认取值为 false。

bool setDataForKey(mmkv::MMBuffer &&data, MMKVKey_t key, bool isDataHolder = false);

数据类型为 string/char* 时,才进行了 true 的赋值。而当 isDataHolder 为 true 时,对 value 的写入会再额外写入一个字段,表示 valueLength。在 Github Discussion 中的讨论,作者解释是为了在写入 string 列表中使用的,而为了代码的统一性就没有再进行区分了。

isKeyEncoded

通过原始 key 长度和将 key 封装为 MMBuffer 的 length 做比较来判断是否已经包含 keyLength 的 protobuf 编码值。实际上在 MMKV_IO.cpp#setDataForKey 中根据 dic 是否存在写入的 key 就决定了 isKeyEncoded 的值,当 dic 中存在写入的 key 时,isKeyEncoded 为 true,表示写入时不需要再将 keyLength 的 protobuf 编码数据写入。

+-----------+-----+
| keyLength | key |
+-----------+-----+

这样做的原因上面其实也提及过,对于 key 的写入其格式如上。当 dic 中存有这个 key,那么说明初始 loadFromFile 或在此之前已经构造了相关的 KeyValueHolder 信息。通过 KeyValueHolder 拿到 offset 数据后,offset 后面的一段内存区数据即为 key 写入所需的格式数据。

// MMKV_IO.cpp#doAppendDataWithKey
KVHolderRet_t
MMKV::doAppendDataWithKey(const MMBuffer &data, const MMBuffer &keyData, bool isDataHolder, uint32_t originKeyLength) {auto isKeyEncoded = (originKeyLength < keyData.length());auto keyLength = static_cast<uint32_t>(keyData.length());auto valueLength = static_cast<uint32_t>(data.length());if (isDataHolder) {valueLength += pbRawVarint32Size(valueLength);}// size needed to encode the keysize_t size = isKeyEncoded ? keyLength : (keyLength + pbRawVarint32Size(keyLength));// size needed to encode the valuesize += valueLength + pbRawVarint32Size(valueLength);SCOPED_LOCK(m_exclusiveProcessLock);bool hasEnoughSize = ensureMemorySize(size);if (!hasEnoughSize || !isFileValid()) {return make_pair(false, KeyValueHolder());}try {// 仍然是区分 key 是否已经编码过了if (isKeyEncoded) {// 直接将 MMBuffer 的数据拷贝写入m_output->writeRawData(keyData);} else {// 写入 protobuf 编码的 keyLength,再写入 key 的值m_output->writeData(keyData);}if (isDataHolder) {m_output->writeRawVarint32((int32_t) valueLength);}m_output->writeData(data); // note: write size of data} catch (std::exception &e) {MMKVError("%s", e.what());return make_pair(false, KeyValueHolder());}auto offset = static_cast<uint32_t>(m_actualSize);auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;m_actualSize += size;updateCRCDigest(ptr, size);return make_pair(true, KeyValueHolder(originKeyLength, valueLength, offset));
}

int 类型数据读取

数据读取内容相对简单点,根据要获取的数据 key,从 dic 中获取到相应的 KeyValueHolder,并将其转换为 MMBuffer 内存单元,读取出映射的指针地址开始的数据。

int32_t MMKV::getInt32(MMKVKey_t key, int32_t defaultValue, bool *hasValue) {if (isKeyEmpty(key)) {if (hasValue != nullptr) {*hasValue = false;}return defaultValue;}SCOPED_LOCK(m_lock);SCOPED_LOCK(m_sharedProcessLock);// 从 dic 中获取数据auto data = getDataForKey(key);if (data.length() > 0) {try {CodedInputData input(data.getPtr(), data.length());if (hasValue != nullptr) {*hasValue = true;}return input.readInt32();} catch (std::exception &exception) {MMKVError("%s", exception.what());}}if (hasValue != nullptr) {*hasValue = false;}return defaultValue;
}MMBuffer MMKV::getDataForKey(MMKVKey_t key) {checkLoadData();{auto itr = m_dic->find(key);if (itr != m_dic->end()) {auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;// 拿到 KeyValueHolder 信息,将其转换为 MMBuffer 数据格式return itr->second.toMMBuffer(basePtr);}}MMBuffer nan;return nan;
}

缺陷

  • 没有类型信息,不支持 getAll MMKV的存储使用 Protobuf 的编码方式,只存储 key 和 value 本身,没有存类型信息。由于没有记录类型信息,MMKV无法自动反序列化,也就无法实现 getAll 接口,因此在需要遍历所有 key-value 的时候(比如迁移数据)就比较棘手了。
  • 文件大小问题 扩容后如果进行 key-value 的删除不会主动 trim size

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/110780.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

批量修改视频尺寸:简单易用的视频剪辑软件教程

如果你需要批量修改视频尺寸&#xff0c;同时保持高质量的画质&#xff0c;那么“固乔剪辑助手”这款软件是你的不二之选。下面就是如何使用这款软件进行批量修改视频尺寸的详细步骤。 1. 首先&#xff0c;你需要在浏览器中进入“固乔科技”的官网&#xff0c;然后下载并安装“…

大数据 DataX 数据同步数据分析入门

目录 一、DataX 概览 1.1 DataX 是什么 1.2 DataX 3.0 概览 设计理念 当前使用现状 二、DataX 详解 2.1 DataX 3.0 框架设计 2.2 DataX 3.0 插件体系 2.3 DataX 3.0 核心架构 2.3.1 核心模块介绍 2.3.2 DataX 调度流程 2.4 DataX 3.0 的六大核心优势 2.4.1 可靠的…

Linux考试复习整理

文章目录 Linux考试整理一.选择题1.用户的密码现象放置在哪个文件夹&#xff1f;2.删除文件或目录的命令是&#xff1f;3.显示一个文件最后几行的命令是&#xff1f;4.删除一个用户并同时删除用户的主目录5.Linux配置文件一般放在什么目录&#xff1f;6.某文件的组外成员的权限…

MongoDB常用脚本汇总

概述 本文汇总记录日常工作中常用的MongoDB查询脚本。 实战 新增 新增集合&#xff1a; db.getSiblingDB("corpus").createCollection(message);删除 删除一条数据&#xff1a; db.getSiblingDB("cx_user").userAccount.deleteOne({_id: ObjectId(6…

科技与时尚共进化,优衣库以硬实力创造品牌长期价值

时尚总是轮回&#xff0c;服装产品如何保持长青&#xff1f;对优衣库来说&#xff0c;产品力不褪色的密码之一&#xff0c;就是始终坚持推动服装科技与时尚融合&#xff0c;赋予生活潮流更多内涵&#xff0c;和更高品质的穿搭体验。 这一点&#xff0c;往往在每年换季新品上市…

2023年【氧化工艺】考试报名及氧化工艺考试总结

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 氧化工艺考试报名是安全生产模拟考试一点通总题库中生成的一套氧化工艺考试总结&#xff0c;安全生产模拟考试一点通上氧化工艺作业手机同步练习。2023年【氧化工艺】考试报名及氧化工艺考试总结 1、【单选题】 由和O…

多维时序 | MATLAB实现SSA-CNN-BiGRU-Attention多变量时间序列预测(SE注意力机制)

多维时序 | MATLAB实现SSA-CNN-BiGRU-Attention多变量时间序列预测&#xff08;SE注意力机制&#xff09; 目录 多维时序 | MATLAB实现SSA-CNN-BiGRU-Attention多变量时间序列预测&#xff08;SE注意力机制&#xff09;预测效果基本描述模型描述程序设计参考资料 预测效果 基本…

企业微信设置可信域名

可信域名的验证文件注意一定放在域名所在的根目录下。 以cloud studio为例&#xff0c;工作区新建终端的路径就是域名在的根目录&#xff0c;而不是服务器的根目录

VA01/VA02/VA03 销售订单根据定价和步骤校验权限隐藏价格

1、业务需求 针对用户使用销售订单时&#xff0c;根据定价和步骤顺序&#xff0c;判断是否有权限&#xff0c;没有权限时隐藏销售订单抬头和行项目的部分价格数据 要限制的定价和步骤在spro中的位置 限制的步骤 2、增强实现 2.1权限对象 创建带有定价和步骤的权限对象 分配…

【华为OD机试python】返回矩阵中非1的元素个数【2023 B卷|200分】

【华为OD机试】-真题 !!点这里!! 【华为OD机试】真题考点分类 !!点这里 !! 题目描述 存在一个m*n的二维数组,其成员取值范围为0,1,2。 其中值为1的元素具备同化特性,每经过1S,将上下左右值为0的元素同化为1。 而值为2的元素,免疫同化。 将数组所有成员随机初始化为0或…

告别黑窗口——C++应用程序界面美化指南

摆脱黑窗口是许多C开发者在设计应用程序时面临的重要问题之一。黑窗口给用户带来了不友好的使用体验&#xff0c;因此改善应用程序界面成为提升用户满意度和使用效果的关键。在本篇博文中&#xff0c;我们将探讨一些方法&#xff0c;帮助你使用C语言实现应用程序界面的美化&…

C++项目开发指导(新员工培训材料)

&#xff08;注&#xff1a;这是一份给新员工的培训材料&#xff0c;集合了实际工作的经验和教训&#xff0c;不一定具有普适性。这份东西大概写于2014-1016年&#xff0c;不涉及之后的社会新气象。特别需要强调的是&#xff0c;这是面向新员工的培训&#xff0c;重点在于破除学…

Jenkins+vue发布项目

在Jenkins 中先创建一个任务名称 然后进行下一步&#xff0c;放一个项目 填写一些参数 参数1&#xff1a; 参数2&#xff1a; 参数3&#xff1a;参数4&#xff1a; 点击保存就行了 配置脚本 // git def git_url http://gitlab.xxxx.git def git_auth_id GITEE_RIVER…

微服务拆分的思考

一、前言 前面几篇文章介绍了微服务核心的两个组件&#xff1a;注册中心和网关&#xff0c;今天我们来思考一下微服务如何拆分&#xff0c;微服务拆分难度在于粒度和层次&#xff0c;粒度太大拆分的意义不大&#xff0c;粒度太小开发、调试、运维会有很多坑。 二、微服务划分…

面试知识点--基础篇

文章目录 前言一、排序1. 冒泡排序2. 选择排序3. 插入排序4. 快速单边循环排序5. 快速双边循环排序6. 二分查找 二、集合1.List2.Map 前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、排序 1. 冒泡排序 冒泡排序就是把小的元素往前调或者把大…

xray的使用

不需要扫描 点击 双击xray 1.打开 2.使用 主打扫描 3.被动扫描 网站 与 Burp 联动 - xray 安全评估工具文档 双击 xray cmd xray_windows_amd64.exe webscan --listen 127.0.0.1:7777 --html-output text.html 1.bp 2.这道这个 3.配置 xray 改为* 4.代理

一些ECharts配置

基于vue3&#xff0c;EChart5.4.3版本 Line <script setup lang"ts"> import {onBeforeUnmount, onMounted, ref, watch} from "vue" import {useEcharts, type ECOption} from "/composables" import * as echarts from "echarts/c…

CSS3 网格布局

CSS3 网格布局&#xff08;CSS Grid Layout&#xff09;是一种强大的布局方式&#xff0c;用于创建复杂的网页布局。它允许你以网格的形式将页面划分为行和列&#xff0c;然后将内容放置在这些行和列的交叉点上。以下是 CSS3 网格布局的基本概念和用法&#xff1a; 1. **创建网…

C++中统计代码的运算时间

在C中&#xff0c;有几种方法可以用来统计代码的运算时间&#xff1a; 使用std::chrono库&#xff1a; C11引入了chrono库&#xff0c;用于处理时间相关的操作。通过使用std::chrono::system_clock和std::chrono::duration_cast&#xff0c;可以很容易地测量代码段的执行时间…

全志A40i PRREMPT-RT Linux平台搭建IgH环境

1、编译安装内核 参考创龙开发板官方文档&#xff0c;在menuconfig中把gmac设置成M&#xff0c;方便卸载原始gmac驱动&#xff0c;然后加载优化后的实时网卡驱动 2、编译IgH 把IgH主站代码放到开发板上&#xff0c;进行配置编译(配置和编译可以参考网上ubuntu…