阿里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. BPT(Business Process Testing)

  2. sourcetree配置gitlab

    一.准备 1.安装git,下载地址:https://git-scm.com/download 安装教程百度一下      git客户端(1.产生gitlab服务端和本地git相互传输时所需要校验的私钥 ...

  3. easyUI学习笔记二

    1.  拖拉大小 <!DOCTYPE html> <html> <head> <title>easyui学习</title> <scr ...

  4. Pycharm2019版官方版本激活码,无需破解

    AHD9079DKZ-eyJsaWNlbnNlSWQiOiJBSEQ5MDc5REtaIiwibGljZW5zZWVOYW1lIjoiSmV0IEdyb3VwcyIsImFzc2lnbmVlTmFtZ ...

  5. Spring学习笔记(13)——aop原理及拦截器

    原理 AOP(Aspect Oriented Programming),也就是面向方面编程的技术.AOP基于IoC基础,是对OOP的有益补充. AOP将应用系统分为两部分,核心业务逻辑(Core bu ...

  6. vue证明题X,vue设置集

    1.开发中的控制台tab格式警告隐藏 出现情况如图: 解决方案:找到此代码,注释掉 2.控制台error报告 出现情况如图 解决方案:找到此代码,替换,对浏览器中的警告进行隐藏 遇到就更

  7. Sql 将A表数据插入到B表

    A表和B表字段不同 --insert into B(Name,PersonalId,Education,IsDel) select Name, PersonId as PersonalId, ( ca ...

  8. ARM-LINUX学习记录

    1:调用C语言函数之前会有一段汇编代码在前面执行来完成软硬件方面的初始化.比如:关闭看门狗:初始化时钟:设置堆栈:调用main函数等.在学习51单片机时候这些操作是由开发环境(如KEIL)在编译C代码 ...

  9. Java使用SSH远程访问Windows并执行命令

    转载于:http://blog.csdn.net/carolzhang8406/article/details/6760430   https://blog.csdn.net/angel_xiaa/a ...

  10. Mybatis基于SqlSession实现CRUD

    之前我们讲的基于XML还是接口注解配置都是使用接口实现CRUD,本文我们将要讲解通过splsession来实现CRUD,这种方法比较灵活. 基本配置 <!-- spring和MyBatis完美整 ...