数据持久化之轻量级Kv持久化(二)

本文详细解析了MMKV库中的内存映射和跨进程数据处理技术,介绍了初始化过程、MMKV对象获取方法、数据编码与写入、文件加载与内存重组,以及AES加密的使用。
摘要由CSDN通过智能技术生成

void MMKV::initializeMMKV(const std::string &rootDir) {

static pthread_once_t once_control = PTHREAD_ONCE_INIT;

pthread_once(&once_control, initialize);

g_rootDir = rootDir;

char *path = strdup(g_rootDir.c_str());

mkPath(path);

free(path);

MMKVInfo(“root dir: %s”, g_rootDir.c_str());

}

获取MMKV对象

获取MMKV对象的方法有以下几个,最傻瓜式的defaultMMKV到最复杂的mmkvWithAshmemID方法,按需调用。

public MMKV defaultMMKV();

public MMKV defaultMMKV(int mode, String cryptKey);

public MMKV mmkvWithID(String mmapID);

public MMKV mmkvWithID(String mmapID, int mode);

public MMKV mmkvWithID(String mmapID, int mode, String cryptKey);

@Nullable

public MMKV mmkvWithAshmemID(Context context, String mmapID, int size, int mode, String cryptKey);

上面的方法,基本都会来到getMMKVWithID方法,然后跳转到MMKV::mmkvWithID里

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

if (mmapID.empty()) {

return nullptr;

}

SCOPEDLOCK(g_instanceLock);

auto itr = g_instanceDic->find(mmapID);

if (itr != g_instanceDic->end()) {

MMKV *kv = itr->second;

return kv;

}

auto kv = new MMKV(mmapID, size, mode, cryptKey);

(*g_instanceDic)[mmapID] = kv;

return kv;

}

g_instanceDic是Map对象,先是根据mmapID在g_instanceDic进行查找,有直接返回,没就新建一个MMKV对象,然后再添加到g_instanceDic里。

MMKV::MMKV(const std::string &mmapID, int size, MMKVMode mode, string *cryptKey)
m_mmapID(mmapID)

, m_path(mappedKVPathWithID(m_mmapID, mode))

, m_crcPath(crcPathWithID(m_mmapID, mode))

, m_metaFile(m_crcPath, DEFAULT_MMAP_SIZE, (mode & MMKV_ASHMEM) ? MMAP_ASHMEM : MMAP_FILE)

, m_crypter(nullptr)

, m_fileLock(m_metaFile.getFd())

, m_sharedProcessLock(&m_fileLock, SharedLockType)

, m_exclusiveProcessLock(&m_fileLock, ExclusiveLockType)

, m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0)

, m_isAshmem((mode & MMKV_ASHMEM) != 0) {

m_fd = -1;

m_ptr = nullptr;

m_size = 0;

m_actualSize = 0;

m_output = nullptr;

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;

}

if (cryptKey && cryptKey->length() > 0) {

m_crypter = new AESCrypt((const unsigned char *) cryptKey->data(), cryptKey->length());

}

m_needLoadFromFile = true;

m_crcDigest = 0;

m_sharedProcessLock.m_enable = m_isInterProcess;

m_exclusiveProcessLock.m_enable = m_isInterProcess;

// sensitive zone

{

SCOPEDLOCK(m_sharedProcessLock);

loadFromFile();

}

}

MMKV的构造函数里,做了一系列参数的构造,分别有:

  • m_mmapID:文件名

  • m_path:存放路径

  • m_crcPath:校验文件存放路径

  • m_metaFile:内存映射的管理对象

  • m_crypter:AES加密密钥

  • m_lock:线程锁

  • m_fileLock:文件锁

  • m_sharedProcessLock:映射文件到内存的锁

  • m_exclusiveProcessLock:在内存读写数据时的锁

  • m_isInterProcess:是否主进程

  • m_isAshmem:是否匿名内存

  • m_ptr:文件映射到内存后的地址

  • m_size:文件大小

  • m_actualSize:内存大小,这个会因为写数据动态变化

  • m_output:Protobuf对象,用于写文件,效率之所高,这里也很关键

  • m_ashmemFile:匿名内存的文件对象

  • m_needLoadFromFile:一个标识对象,用于是否加载过文件,加载过就将它置为false

  • m_crcDigest:数据校验

MMKV对象构造完毕后,会将该对象的指针地址返回给Java层,Java层的MMKV类会保存住该地址,用于接下来的读写操作。

public static MMKV mmkvWithID(String mmapID, int mode, String cryptKey) {

long handle = getMMKVWithID(mmapID, mode, cryptKey);

return new MMKV(handle);

}

写数据

以写入String对象为例,看看写入步骤

public boolean encode(String key, String value) {

return encodeString(nativeHandle, key, value);

}

来到MMKV::setStringForKey方法

bool MMKV::setStringForKey(const std::string &value, const std::string &key) {

if (key.empty()) {

return false;

}

auto data = MiniPBCoder::encodeDataWithObject(value);

return setDataForKey(std::move(data), key);

}

MiniPBCoder::encodeDataWithObject方法将value构造出一个Protobuf数据对象(本章不对此详细分析),然后将构造出来的数据对象通过std::move方法传到setDataForKey里

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

if (data.length() == 0 || key.empty()) {

return false;

}

SCOPEDLOCK(m_lock);

SCOPEDLOCK(m_exclusiveProcessLock);

checkLoadData();

// m_dic[key] = std::move(data);

auto itr = m_dic.find(key);

if (itr == m_dic.end()) {

itr = m_dic.emplace(key, std::move(data)).first;

} else {

itr->second = std::move(data);

}

return appendDataWithKey(itr->second, key);

}

  • checkLoadData()用来检查文件有效性(本章不对此详细分析)

  • m_dic是一个Map对象,在这里判断是否已经存在该Key,有就替换,没就添加

  • appendDataWithKey()是将该对象添加到内存里(本章不对此详细分析)

读数据

public String decodeString(String key, String defaultValue) {

return decodeString(nativeHandle, key, defaultValue);

}

来到MMKV::getDataForKey方法

const MMBuffer &MMKV::getDataForKey(const std::string &key) {

SCOPEDLOCK(m_lock);

checkLoadData();

auto itr = m_dic.find(key);

if (itr != m_dic.end()) {

return itr->second;

}

static MMBuffer nan(0);

return nan;

}

通过key在m_dic对象里进行查找,如果查找到,就返回,没则返回一个0长度的对象。

##2.2 MMAP映射

####加载文件

void MMKV::loadFromFile() {

// 匿名内存的加载,本章不深入分析

if (m_isAshmem) {

loadFromAshmem();

return;

}

m_metaInfo.read(m_metaFile.getMemory());

/* O_RDWR:读、写打开

  • O_CREAT:若此文件不存在则创建它。使用此选择项时,需同时说明第三个参数mode,用其说明该新文件的存取许可权位。

  • S_IRWXU:模式标志:由用户读,写,执行。

*/

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_cast<size_t>(st.st_size);

}

// 对齐操作,mmap的使用要求

// round up to (n * pagesize)

if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {

size_t oldSize = m_size;

m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;

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 = static_cast<size_t>(st.st_size);

}

zeroFillFile(m_fd, oldSize, m_size - oldSize);

}

// MMKV的核心之一,使用mmap函数的MAP_SHARED来实现文件和内存形成映射,只要修改内存的数据,这个函数会自动的帮我们写到文件里,非常好用。

m_ptr = (char *) mmap(nullptr, 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));

} else {

// 读取现在文件里数据的长度

memcpy(&m_actualSize, m_ptr, Fixed32Size);

MMKVInfo(“loading [%s] with %zu size in total, file size is %zu”, m_mmapID.c_str(),

m_actualSize, m_size);

bool loaded = false;

if (m_actualSize > 0) {

if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {

// 检查数据的有效性,MMKV的WIKI上说道微信每天都几十万次校验不通过的情况,恐怖如斯

if (checkFileCRCValid()) {

MMKVInfo(“loading [%s] with crc %u sequence %u”, m_mmapID.c_str(),

m_metaInfo.m_crcDigest, m_metaInfo.m_sequence);

// 读取数据到内存里

MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);

// 如果是加密的话,得先解密

if (m_crypter) {

decryptBuffer(*m_crypter, inputBuffer);

}

// 将内存的数据反序列化到Map里,m_dic是个Map

m_dic = MiniPBCoder::decodeMap(inputBuffer);

m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize,

m_size - Fixed32Size - m_actualSize);

loaded = true;

}

}

}

if (!loaded) {

SCOPEDLOCK(m_exclusiveProcessLock);

if (m_actualSize > 0) {

writeAcutalSize(0);

}

m_output = new CodedOutputData(m_ptr + Fixed32Size, m_size - Fixed32Size);

recaculateCRCDigest();

}

MMKVInfo(“loaded [%s] with %zu values”, m_mmapID.c_str(), m_dic.size());

}

}

if (!isFileValid()) {

MMKVWarning(“[%s] file not valid”, m_mmapID.c_str());

}

m_needLoadFromFile = false;

}

####参数解释

mmap函数是MMKV的干货之一了,如果没有这个函数的存在,或许就没有今天的MMKV了,下面说下这个函数的参数和使用方法。

mmap (一种内存映射文件的方法)

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。

头文件 <sys/mman.h>

####函数原型

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

start:映射区的开始地址。设置null即可。

length:映射区的长度。传入文件对齐后的大小m_size。

prot:期望的内存保护标志,不能与文件的打开模式冲突。设置可读可写。

flags:指定映射对象的类型,映射选项和映射页是否可以共享。设置MAP_SHARED表示可进程共享,MMKV之所以可以实现跨进程使用,这里是关键。

fd:有效的文件描述词。用上面所打开的m_fd。

off_toffset:被映射对象内容的起点。从头开始,比较好理解。

####内存重组

在跨进程读写的时候,进程A修改了一个数据,进程B去读的时候,就会校验内存的数据和文件的数据,一旦不相同,就说明有了跨进程的操作,这个时候就需要内存重组,清空原有数据,重新读最新的文件映射到内存中。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

针对于上面的问题,我总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。
Android进阶视频+面试资料部分截图

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

图片转存中…(img-AyL6wqdE-1712425094357)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

针对于上面的问题,我总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。
[外链图片转存中…(img-SF8d220G-1712425094357)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 27
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值