阿里P7Android高级架构进阶视频免费学习请点击:https://space.bilibili.com/474380680
本篇文章将继续从以下两个内容来介绍轻量级Kv持久化:

  • [SharedPreferences详解与原理分析]
  • [ 微信MMKV源码分析]

一、SharedPreferences详解与原理分析

SharedPreferences作为Android存储数据方式之一,主要特点是:

只支持Java基本数据类型,不支持自定义数据类型;
应用内数据共享;
使用简单.
使用方法
1、存数据

SharedPreferences sp = getSharedPreferences("sp_demo", Context.MODE_PRIVATE);
sp.edit().putString("name", "小张").putInt("age", 11).commit();

或者下面的写法也可以

SharedPreferences sp = getSharedPreferences("sp_demo", Context.MODE_PRIVATE);
Editor editor = sp.edit();
editor.putString("name", "小张");
editor.putInt("age", 11);
editor.commit();

切记不要写成下面的形式,会导致数据无法存储

SharedPreferences sp = getSharedPreferences("sp_demo", Context.MODE_PRIVATE);
sp.edit().putString("name", "小张");
sp.edit().putInt("age", 11);
sp.edit().commit();

为什么这种方式无法存储,因为sp.edit()每次都会返回一个新的Editor对象,Editor的实现类EditorImpl里面会有一个缓存的Map,最后commit的时候先将缓存里面的Map写入内存中的Map,然后将内存中的Map写进XML文件中。使用上面的方式commit,由于sp.edit()又重新返回了一个新的Editor对象,缓存中的Map是空的,所以导致数据无法被存储。

2、取数据

SharedPreferences sp = getSharedPreferences("sp_demo", Context.MODE_PRIVATE);
String name = sp.getString("name", null);
int age = sp.getInt("age", 0);

getSharedPreferences的具体实现是在frameworks/base/core/java/android/app/ContextImpl.java,代码如下:

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
......
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
} ......
sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
......
return sp;
}

SharedPreferencesImpl是SharedPreferences接口的具体实现类,一个name对应一个SharedPreferencesImpl,一个应用程序中根据name的不同会有多个SharedPreferencesImpl。
SharedPreferencesImpl的具体实现是在frameworks/base/core/java/android/app/SharedPreferencesImpl.java,我们可以通过getSharedPreferences获得SharedPreferences的实例,当我们调用sp.getString等get方法取数据时,实际上是直接从内存中的Map里面去取,get方法传入的第一个参数正好是Map的key,第二个参数是当Map中没有这个key对应值的时候,返回的默认值。

二、微信MMKV源码分析

2.1整体流程

初始化

在使用MMKV框架前,需调用以下方法进行初始化

MMKV.initialize(context);

这里的Java层主要是获取到保存文件的路径,传入Native层,这里默认的路径是APP的内部存储目录下的mmkv路径,这里不支持修改,如需修改,需将源码clone下来手动修改编译了。

public static String initialize(Context context) {
String rootDir = context.getFilesDir().getAbsolutePath() + "/mmkv";
initialize(rootDir);
return rootDir;
}

到了Native层,通过Java_com_tencent_mmkv_MMKV_initialize方法跳转到MMKV::initializeMMKV方法里,启动了一个线程做初始化,然后检查内部路径是否存在,不存在则创建之。

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去读的时候,就会校验内存的数据和文件的数据,一旦不相同,就说明有了跨进程的操作,这个时候就需要内存重组,清空原有数据,重新读最新的文件映射到内存中。

void MMKV::checkLoadData() {
// 检查是否已经加载过数据
if (m_needLoadFromFile) {
SCOPEDLOCK(m_sharedProcessLock); m_needLoadFromFile = false;
loadFromFile();
return;
}
if (!m_isInterProcess) {
return;
} // TODO: atomic lock m_metaFile?
MMKVMetaInfo 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); clearMemoryState();
loadFromFile();
}
// 比较下crc校验码
else if (m_metaInfo.m_crcDigest != metaInfo.m_crcDigest) {
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) {
MMKVInfo("file size has changed [%s] from %zu to %zu", m_mmapID.c_str(), m_size,
fileSize);
clearMemoryState();
loadFromFile();
} else {
partialLoadFromFile();
}
}
}

阿里P7Android高级架构进阶视频免费学习请点击:https://space.bilibili.com/474380680
参考https://juejin.im/post/5baf8ae8f265da0ae92a7df5
https://juejin.im/post/5bac285d5188255c7039ab80
https://blog.csdn.net/lyl278401555/article/details/50610790

数据持久化之轻量级Kv持久化(二)的更多相关文章

  1. 轻量级Java持久化框架,Hibernate完美助手,Minidao 1.6.2版本发布

    Minidao 1.6.2 版本发布,轻量级Java持久化框架(Hibernate完美助手) Minidao产生初衷? 采用Hibernate的J2EE项目都有一个痛病,针对复杂业务SQL,hiber ...

  2. 洛谷 P3919 【模板】可持久化数组(可持久化线段树/平衡树)-可持久化线段树(单点更新,单点查询)

    P3919 [模板]可持久化数组(可持久化线段树/平衡树) 题目背景 UPDATE : 最后一个点时间空间已经放大 标题即题意 有了可持久化数组,便可以实现很多衍生的可持久化功能(例如:可持久化并查集 ...

  3. 洛谷——P3919 【模板】可持久化数组(可持久化线段树/平衡树)

    P3919 [模板]可持久化数组(可持久化线段树/平衡树) 题目背景 UPDATE : 最后一个点时间空间已经放大 标题即题意 有了可持久化数组,便可以实现很多衍生的可持久化功能(例如:可持久化并查集 ...

  4. 大数据实践:ODI 和 Twitter (二)

    大数据实践:ODI和Twitter(二) 在前面的文章中,我们已经使用flume将数据从twitter抓取到Hive中,现在我们来看看ODI(Oracle Data Integrator)如何在HIV ...

  5. Luogu P3919【模板】可持久化数组(可持久化线段树/平衡树)

    题面:[模板]可持久化数组(可持久化线段树/平衡树) 不知道说啥,总之我挺喜欢自己打的板子的! #include<cstdio> #include<cstring> #incl ...

  6. CYQ.Data 轻量数据层之路 使用篇二曲 MAction 数据查询(十三)----002

    原文链接:https://blog.csdn.net/cyq1162/article/details/53303390 前言说明: 本篇继续上一篇内容,本节介绍所有相关查询的使用. 主要内容提要: 1 ...

  7. python + docker, 实现天气数据 从FTP获取以及持久化(二)-- python操作MySQL数据库

    前言 在这一节中,我们主要介绍如何使用python操作MySQL数据库. 准备 MySQL数据库使用的是上一节中的docker容器 “test-mysql”. Python 操作 MySQL 我们使用 ...

  8. python + docker, 实现天气数据 从FTP获取以及持久化(五)-- 利用 Docker 容器化 Python 程序

    背景 不知不觉中,我们已经完成了所有的编程工作.接下来,我们需要把 Python 程序 做 容器化 (Docker)部署. 思考 考虑到项目的实际情况,“持久化天气”的功能将会是一个独立的功能模块发布 ...

  9. python + docker, 实现天气数据 从FTP获取以及持久化(一)

    前情提要 最近项目需要天气数据(预报和历史数据)来作为算法程序的输入. 项目的甲方已经购买了天气数据, 依照他们的约定,天气数据的供应商会将数据以"文本" (.TXT)的方式发到F ...

随机推荐

  1. SAP内表查询速度优化实例-OPEN SQL

    一.FOR ALL ENTRIES IN 案例 今天碰到工单报工统计分析表查询速度特别慢 经查看源代码: SELECT afpo~dwerk afko~aufnr afpo~matnr AS plnb ...

  2. [FW]修复ubutnu12.04+win7的grub2引导

    [转]修复ubutnu12.04+win7的grub2引导 原文位置:http://wenku.baidu.com/view/b6b7c9926bec0975f465e2f8.html ps:我使用的 ...

  3. Introduction to Object-Oriented JavaScript 转载自:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript

    Introduction to Object-Oriented JavaScript IN THIS ARTICLE JavaScript review Object-oriented program ...

  4. IIS Express 不允许的父路径

    IIS Express 启动一个asp的网站,出现一个错误 Active Server Pages 错误 'ASP 0131' 不允许的父路径 对于IIS可以在可视化的IIS Manager中配置: ...

  5. java中多种方式解析xml

    第一种:DOM.DOM的全称是Document Object Model,也即文档对象模型.在应用程序中,基于DOM的XML分析器将一个XML文档转换成一个对象模型的集合(通常称DOM树),应用程序正 ...

  6. LeetCode Linked List Easy 83. Remove Duplicates from Sorted List

    Description Given a sorted linked list, delete all duplicates such that each element appear only onc ...

  7. mqtt haproxy 代理及负载搭建

    目录 mqtt 分布集群搭建 haproxy 安装配置 解压 安装 配置haproxy.cfg 启动haproxy 配置mqtt 测试 负载配置说明 负载均衡算法 ACL规则定义 全局配置 默认配置 ...

  8. 网络通信_socket

    socket又称套接字 使用server实现循环通信 代码如下 import socket server = socket.socket() server.bind(()) server.listen ...

  9. 2019牛客多校第七场H Pair 数位DP

    题意:给你一个3个数A, B, C问有多少对pair(i, j),1 <= i <= A, 1 <= j <= B, i AND j > C或 i XOR j < ...

  10. PNG文件格式

    PNG文件的组成 一个PNG文件可以看作是由多个数据块(chunk)部分组成,如同积木一样,一个数据块就是一个小积木,不同类型的积木组合搭建成了我们的PNG图像. PNG图像至少由文件署名域和三个关键 ...