SharedPreferences+MMAP+MMKV数据持久化原理解析

SharedPreferences

介绍

  • SharedPreferences(以下统称为sp)是Android提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。
  • 由于sharedPreferences是基于xml文件实现的,所有持久化数据都是一次性加载,如果数据过大是不适合采用SP存放。
  • 实际上是用xml文件存放数据,文件存保存放在/data/data//shared_prefs/

    如何使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    SharedPreferences setting = getSharedPreferences("hello",MODE_PRIVATE);
    //让setting处于编辑状态
    SharedPreferences.Editor editor = setting.edit();
    //存放数据
    editor.putString("name","jacky");
    //完成提交
    editor.commit();
    // editor.apply();;
    //读取信息
    String name = setting.getString("name","0");

源码分析

获取

如何获取SharedPreferences对象?getSharedPreferences方法是在ContextWrapper中,所以可以在Activity中直接获取

1
2
3
4
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}

实现在ContextImpl中的getSharedPreferences方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
return sp;
}

初始化

初始化:通过File读取文件,加载数据,然后通过XmlUtils解析文件。
SharedPreferencesImpl -> startLoadFromDisk -> loadFromDisk ->

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void loadFromDisk() {
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
}catch (Throwable t) {
thrown = t;
}
}

commit

commit -> enqueueDiskWrite -> writeToFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public boolean commit() {
long startTime = 0;

if (DEBUG) {
startTime = System.currentTimeMillis();
}

MemoryCommitResult mcr = commitToMemory();

SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//这个地方就让主线程卡住的原因,如果多个commit,就需要等待,就会阻塞。
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}

private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);

final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};

// 未true是commit提交,在当前线程提交,注意这里的锁.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// apply 如队列
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

在commit方法中,首先执行写入任务也就是enqueueDiskWrite这个方法,然后让调用线程处于等待状态,当写入任务执行成功后唤起调用commit的线程,假设调用commit的线程就是主线线程,并且写入任务耗时还比较多的,这不就阻塞住主线程了吗?

writeToFile

直接io操作写入的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
long startTime = 0;
long existsTime = 0;
long backupExistsTime = 0;
long outputStreamCreateTime = 0;
long writeTime = 0;
long fsyncTime = 0;
long setPermTime = 0;
long fstatTime = 0;
long deleteTime = 0;
try {
FileOutputStream str = createFileOutputStream(mFile);

if (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}

if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

writeTime = System.currentTimeMillis();

FileUtils.sync(str);

fsyncTime = System.currentTimeMillis();

str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
}

apply

apply -> 异步延迟加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public void apply() {
final long startTime = System.currentTimeMillis();

final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}

if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
// 这个是保存到QueuedWork队列, 添加到LinkedList<Runnable> sFinishers = new LinkedList<>();里
QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}

入队列,创建handler,sWork队列中数据最终在queued-work-looper 线程中依次得到执行

1
2
3
4
5
6
7
8
9
10
11
12
13
 public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();

synchronized (sLock) {
sWork.add(work);

if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}

最终processPendingWork执行,实际上就是for循环,轮询执行;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static void processPendingWork() {
long startTime = 0;

if (DEBUG) {
startTime = System.currentTimeMillis();
}

synchronized (sProcessingWork) {
LinkedList<Runnable> work;

synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();

// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}

if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}

apply的中写入操作也是在异步线程执行,不会导致主线程卡顿,但是如果异步任务执行时间过长,当ActvityThread执行了handleStopActivity或者handleServiceArgs或者handlePauseActivity 等方法的时候都会调用QueuedWork.waitToFinish()方法,而此方法中会在异步任务执行完成前一直阻塞住主线程,所以卡顿问题就产生了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;

Handler handler = getHandler();

synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);

if (DEBUG) {
hadMessages = true;
Log.d(LOG_TAG, "waiting");
}
}

// We should not delay any work as this might delay the finishers
sCanDelay = false;
}

StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}

try {
while (true) {
Runnable finisher;

synchronized (sLock) {
finisher = sFinishers.poll();
}

if (finisher == null) {
break;
}

finisher.run();
}
} finally {
sCanDelay = true;
}

synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;

if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;

if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
mWaitTimes.log(LOG_TAG, "waited: ");
}
}
}
}

会从sFinishers队列中取出数据然后执行run方法,我们别忘了在apply的方法中,我们还添加了QueuedWork.addFinisher(awaitCommit);这个awaitCommit 就得到执行了但是awaitCommit中的代码确实是阻塞的代码,等待写入线程执行完毕才能唤起此线程。如果 apply中的写入代码不执行完,主线程就一直卡住了,也就出现了我们上面的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}

if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};

读数据

1
2
3
4
5
6
7
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}

关键awaitLoadedLocked 这个方法,当数据没有加载完,就让调用的线程处于等待中,阻塞住了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}

所以获取数据也是阻塞的。

源码总结

从上面可以看出两者最后都是先调用commitToMemory,将更改提交到内存,在这一点上两者是一致的,之后又都调用了enqueueDiskWrite进行数据持久化任务,不过commit函数一般会在当前线程直接写文件,而apply则提交到一个队列里,延迟加载,之后直接返回。
(这里代码是android-28,可能新版本是是一个线程池,而非队列)

总结

  • xml格式保存
  • 通过子线程使用IO读取整个文件,并进行xml解析,存入内存Map,完成初始化,默认大小是16k。
  • commit是同步提交,阻塞调用的线程,为啥?如果是使用commit方式提交,会阻塞调用commit方法的线程,如果写入任务很多比较耗时,就卡住了,所以不要在主线程执行写入文件的操作,否则阻塞主线程;apply是异步(延迟)提交,无法获取结果且可能数据丢失。apply 放法不会阻塞调用的线程,但是如果写入任务比较耗时,会阻塞住主线程,因为主线程有调用的代码,需要等写入任务执行完了才会继续往下执行。
  • 更新,是吧map中数据,全部序列化XML,覆盖文件保存(全量更新)。
    所以有没有一种方案改进sp的xml,io,并发问题?

MMAP

传统I/O

虚拟内存被操作系统划分为两块:用户空间和内核空间,用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方,内核空间由所有进程共享。为了安全,他们是隔离的,即使用户的程序崩溃了,内核也不受影响。
写文件的流程:

1、调用write,告诉内核需要写入数据的开始地址与长度。
2、内核将数据拷贝到内核页缓存。
3、由操作系统调用,将数据拷贝到磁盘,完成写入。

MMMAP

Linux 通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping).
对文件进行mmap,会在进程的虚拟内存分配地址空间创建映射关系。实现这样的映射关系后,就可以采用指针的方式读写操作这一段内存,而系统会自动回写到对应的文件磁盘上。

特点

  • MMAP对文件的读写操作只需要从磁盘到用户主存的一次数据拷贝过程,减少了数据的拷贝次数,提高了文件操作效率
  • MMAP使用逻辑内存对磁盘文件进行映射,操作内存就相当于操作文件,不需要开启线程,操作MMAP的速度和操作内存的速度一样快
  • MMAP提供一段可供随时写入的内存块,App只管往里面写数据,由操作系统如内存不足、进程退出等时候负责将内存回写到文件。

    案例

    微信Mars:
    github
    文档
    美团Logan:
    github
    博客
    网易android-mmap:

    Binder通信

    MMKV

    MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。

    原理

  • 内存准备
    通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
  • 数据组织
    数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
  • 写入优化
    考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
  • 空间增长
    使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。

    源码

    初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir, jstring cacheDir, jint logLevel) {
    if (!rootDir) {
    return;
    }
    // 获取rootDir的url char指针数组字符串,调⽤MMKV::initializeMMKV进⼀步初始化。
    const char *kstr = env->GetStringUTFChars(rootDir, nullptr);
    if (kstr) {
    MMKV::initializeMMKV(kstr, (MMKVLogLevel) logLevel);
    env->ReleaseStringUTFChars(rootDir, kstr);

    g_android_tmpDir = jstring2string(env, cacheDir);
    }
    }

    ThreadOnceToken_t once_control = ThreadOnceUninitialized;
    void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
    g_currentLogLevel = logLevel;
    ThreadLock::ThreadOnce(&once_control, initialize);
    // 获取rootDir的url char指针数组字符串
    g_rootDir = rootDir;
    // 根据路径创建⽂件夹
    mkPath(g_rootDir);
    }

获取MMKV对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
MMKV *MMKV::mmkvWithID(const string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath) {

if (mmapID.empty()) {
return nullptr;
}
// 加锁
SCOPED_LOCK(g_instanceLock);
// 将 mmapID 与 relativePath 结合生成 mmapKey
auto mmapKey = mmapedKVKey(mmapID, rootPath);
// 通过 mmapKey 在 map 中查找对应的 MMKV 对象并返回
auto itr = g_instanceDic->find(mmapKey);
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second;
return kv;
}
// 如果找不到,构建路径后构建 MMKV 对象并加入 map
if (rootPath) {
MMKVPath_t specialPath = (*rootPath) + MMKV_PATH_SLASH + SPECIAL_CHARACTER_DIRECTORY_NAME;
if (!isFileExist(specialPath)) {
mkPath(specialPath);
}
MMKVInfo("prepare to load %s (id %s) from rootPath %s", mmapID.c_str(), mmapKey.c_str(), rootPath->c_str());
}

//构造对象
auto kv = new MMKV(mmapID, mode, cryptKey, rootPath);
kv->m_mmapKey = mmapKey;
(*g_instanceDic)[mmapKey] = kv;
return kv;
}

获取path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
extern bool mkPath(const MMKVPath_t &str) {
// strdup拷贝⼀份字符串到path中。
char *path = strdup(str.c_str());

struct stat sb = {};
bool done = false;
char *slash = path;

while (!done) {
// strspn 是⼀直找到匹配字符串,直到出现第⼀个不是"/"
slash += strspn(slash, "/");
// strcspn 则是⼀直找不匹配的字符串,直到出现第⼀个“/”
slash += strcspn(slash, "/");
// 经过这样拆解,就能把路径⼀个个分割开。通过这中⽅式就能直到什么时候遍历完整个路径。
done = (*slash == '\0');
*slash = '\0';

// stat获取path每⼀个⽂件夹的权限状态,必须保证每⼀级别的⽂件都是0777,也就是读写执⾏全部权限打开。

if (stat(path, &sb) != 0) {
if (errno != ENOENT || mkdir(path, 0777) != 0) {
MMKVWarning("%s : %s", path, strerror(errno));
free(path);
return false;
}
} else if (!S_ISDIR(sb.st_mode)) {
MMKVWarning("%s: %s", path, strerror(ENOTDIR));
free(path);
return false;
}

*slash = '/';
}
free(path);

return true;
}

构造对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
MMKV::MMKV(const string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath){
.......
// 通过加密 key 构建 AES 加密对象 AESCrypt
# ifndef MMKV_DISABLE_CRYPT
if (cryptKey && cryptKey->length() > 0) {
m_dicCrypt = new MMKVMapCrypt();
m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
} else {
m_dic = new MMKVMap();
}
# else
m_dic = new MMKVMap();
# endif
...................................
m_lock->initialize();
................
// sensitive zone
// 加锁后通过 loadFromFile 方法从文件中读取数据,这里的锁是一个跨进程的文件共享锁;
{
SCOPED_LOCK(m_sharedProcessLock);
loadFromFile();
}
}
void initialize() {
g_instanceDic = new unordered_map<string, MMKV *>;
// 初始化了⼀个全局的线程锁
g_instanceLock = new ThreadLock();
g_instanceLock->initialize();
// 。在MMKV中,设置好每⼀页(page)的⼤⼩,⼀般来说我们在32位的机⼦中⼀页都是
4kb⼤⼩
mmkv::DEFAULT_MMAP_SIZE = mmkv::getPageSize();
}

加载数据loadFromFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
void MMKV::loadFromFile() {
//读缓存
if (m_metaFile->isFileValid()) {
m_metaInfo->read(m_metaFile->getMemory());
}
//读文件
if (!m_file->isFileValid()) {
m_file->reloadFromFile();
}

// loading 开始加载
if (loadFromFile && m_actualSize > 0) {
MMKVInfo("loading [%s] with crc %u sequence %u version %u", m_mmapID.c_str(), m_metaInfo->m_crcDigest,
m_metaInfo->m_sequence, m_metaInfo->m_version);
// 读取 MMBuffer
MMBuffer inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
// 如果需要解密,对文件进行解密
if (m_crypter) {
clearDictionary(m_dicCrypt);
} else {
clearDictionary(m_dic);
}
// 通过 MiniPBCoder 将 MMBuffer 转换为 Map
if (needFullWriteback) {
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
MiniPBCoder::greedyDecodeMap(*m_dicCrypt, inputBuffer, m_crypter);
} else
#endif
{
MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer);
}
} else {
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
MiniPBCoder::decodeMap(*m_dicCrypt, inputBuffer, m_crypter);
} else
#endif
{
MiniPBCoder::decodeMap(*m_dic, inputBuffer);
}
}
// 构造用于输出的 CodeOutputData
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
m_output->seek(m_actualSize);
if (needFullWriteback) {
fullWriteback();
}
} else {
// file not valid or empty, discard everything
SCOPED_LOCK(m_exclusiveProcessLock);

m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
if (m_actualSize > 0) {
writeActualSize(0, 0, nullptr, IncreaseSequence);
sync(MMKV_SYNC);
} else {
writeActualSize(0, 0, nullptr, KeepSequence);
}
}
}

void MemoryFile::reloadFromFile() {
....................
if (!m_diskFile.open()) {
MMKVError("fail to open:%s, %s", m_diskFile.m_path.c_str(), strerror(errno));
} else {
FileLock fileLock(m_diskFile.m_fd);
InterProcessLock lock(&fileLock, ExclusiveLockType);
SCOPED_LOCK(&lock);
// 页文件大小,32位大概是4k
mmkv::getFileSize(m_diskFile.m_fd, m_size);
// round up to (n * pagesize)
// 将文件大小对齐到页大小的整数倍,用 0 填充不足的部分
if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
size_t roundSize = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
truncate(roundSize);
} else {
auto ret = mmap();
if (!ret) {
doCleanMemoryCache(true);
}
}
# ifdef MMKV_IOS
tryResetFileProtection(m_diskFile.m_path);
# endif
}
}
bool File::open() {
// 打开对应的文件
m_fd = ::open(m_path.c_str(), OpenFlag2NativeFlag(m_flag), S_IRWXU);
if (!isFileValid()) {
MMKVError("fail to open [%s], %d(%s)", m_path.c_str(), errno, strerror(errno));
return false;
}
MMKVInfo("open fd[%p], %s", m_fd, m_path.c_str());
return true;
}

通过 mmap 将文件映射到内存

1
2
3
4
5
6
7
8
9
10
11
bool MemoryFile::mmap() {
//通过 mmap 将文件映射到内存
m_ptr = (char *) ::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_diskFile.m_fd, 0);
if (m_ptr == MAP_FAILED) {
MMKVError("fail to mmap [%s], %s", m_diskFile.m_path.c_str(), strerror(errno));
m_ptr = nullptr;
return false;
}

return true;
}

写入

Java 层的 MMKV 对象继承了 SharedPreferences 及 SharedPreferences.Editor 接口并实现了一系列如 putInt、putLong 的方法用于对存储的数据进行修改;最终还是会进入c方法:

1
2
3
4
5
6
7
8
9
10
11
12
bool MMKV::set(int32_t value, MMKVKey_t key) {
if (isKeyEmpty(key)) {
return false;
}
size_t size = pbInt32Size(value);
MMBuffer data(size);
// 构造值对应的 MMBuffer,通过 CodedOutputData 将其写入 Buffer
CodedOutputData output(data.getPtr(), size);
output.writeInt32(value);

return setDataForKey(move(data), key);
}

获取到了写入的 value 在 protobuf 中所占据的大小,之后为其构造了对应的 MMBuffer 并将数据写入了这段 Buffer,最后调用到了 setDataForKey 方法;同时可以发现 CodedOutputData 是与 Buffer 交互的桥梁,可以通过它实现向 MMBuffer 中写入数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key, bool isDataHolder) {
if ((!isDataHolder && data.length() == 0) || isKeyEmpty(key)) {
return false;
}
// 获取写锁
SCOPED_LOCK(m_lock);
SCOPED_LOCK(m_exclusiveProcessLock);
// 确保数据已读入内存
checkLoadData();

#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
if (isDataHolder) {
auto sizeNeededForData = pbRawVarint32Size((uint32_t) data.length()) + data.length();
if (!KeyValueHolderCrypt::isValueStoredAsOffset(sizeNeededForData)) {
data = MiniPBCoder::encodeDataWithObject(data);
isDataHolder = false;
}
}
// 将 data 写入 map 中
auto itr = m_dicCrypt->find(key);
if (itr != m_dicCrypt->end()) {
# ifdef MMKV_APPLE
auto ret = appendDataWithKey(data, key, itr->second, isDataHolder);
# else
auto ret = appendDataWithKey(data, key, isDataHolder);
# endif
if (!ret.first) {
return false;
}
if (KeyValueHolderCrypt::isValueStoredAsOffset(ret.second.valueSize)) {
KeyValueHolderCrypt kvHolder(ret.second.keySize, ret.second.valueSize, ret.second.offset);
memcpy(&kvHolder.cryptStatus, &t_status, sizeof(t_status));
itr->second = move(kvHolder);
} else {
itr->second = KeyValueHolderCrypt(move(data));
}
} else {
auto ret = appendDataWithKey(data, key, isDataHolder);
if (!ret.first) {
return false;
}
if (KeyValueHolderCrypt::isValueStoredAsOffset(ret.second.valueSize)) {
auto r = m_dicCrypt->emplace(
key, KeyValueHolderCrypt(ret.second.keySize, ret.second.valueSize, ret.second.offset));
if (r.second) {
memcpy(&(r.first->second.cryptStatus), &t_status, sizeof(t_status));
}
} else {
m_dicCrypt->emplace(key, KeyValueHolderCrypt(move(data)));
}
}
} else
#endif // MMKV_DISABLE_CRYPT
{
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));
}
}
m_hasFullWriteback = false;
#ifdef MMKV_APPLE
[key retain];
#endif
return true;
}

数据已读入内存的情况下将 data 写入了对应的 map,之后调用了 appendDataWithKey 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

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
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);
// 确定剩余映射空间足够
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);
}

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 key
size_t size = isKeyEncoded ? keyLength : (keyLength + pbRawVarint32Size(keyLength));
// size needed to encode the value
size += valueLength + pbRawVarint32Size(valueLength);

SCOPED_LOCK(m_exclusiveProcessLock);

bool hasEnoughSize = ensureMemorySize(size);
if (!hasEnoughSize || !isFileValid()) {
return make_pair(false, KeyValueHolder());
}

#ifdef MMKV_IOS
auto ret = guardForBackgroundWriting(m_output->curWritePointer(), size);
if (!ret.first) {
return make_pair(false, KeyValueHolder());
}
#endif
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
if (KeyValueHolderCrypt::isValueStoredAsOffset(valueLength)) {
m_crypter->getCurStatus(t_status);
}
}
#endif
// 重新构建并写入数据
try {
if (isKeyEncoded) {
m_output->writeRawData(keyData);
} else {
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;
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
m_crypter->encrypt(ptr, ptr, size);
}
#endif
m_actualSize += size;
updateCRCDigest(ptr, size);

return make_pair(true, KeyValueHolder(originKeyLength, valueLength, offset));
}

void CodedOutputData::writeRawData(const MMBuffer &data) {
size_t numberOfBytes = data.length();
if (m_position + numberOfBytes > m_size) {
auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) +
", m_size: " + to_string(m_size);
throw out_of_range(msg);
}
memcpy(m_ptr + m_position, data.getPtr(), numberOfBytes);
m_position += numberOfBytes;
}

注意的是:由于 protobuf 不支持增量更新,为了避免全量写入带来的性能问题,MMKV 在文件中的写入并不是通过修改文件对应的位置,而是直接在后面 append 一条新的数据,即使是修改了已存在的 key。而读取时只记录最后一条对应 key 的数据,这样显然会在文件中存在冗余的数据。这样设计的原因我认为是出于性能的考量,MMKV 中存在着一套内存重整机制用于对冗余的 key-value 数据进行处理。它正是在确保内存充足时实现的;

内存重整ensureMemorySize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

// since we use append mode, when -[setData: forKey:] many times, space may not be enough
// try a full rewrite to make space
bool MMKV::ensureMemorySize(size_t newSize) {
// 如果内存剩余大小不足以写入,尝试进行内存重整,将 map 中的数据重新写入 protobuf 文件
if (newSize >= m_output->spaceLeft() || (m_crypter ? m_dicCrypt->empty() : m_dic->empty())) {
// try a full rewrite to make space
auto fileSize = m_file->getFileSize();
auto preparedData = m_crypter ? prepareEncode(*m_dicCrypt) : prepareEncode(*m_dic);
auto sizeOfDic = preparedData.second;
size_t lenNeeded = sizeOfDic + Fixed32Size + newSize;
size_t dicCount = m_crypter ? m_dicCrypt->size() : m_dic->size();
size_t avgItemSize = lenNeeded / std::max<size_t>(1, dicCount);
size_t futureUsage = avgItemSize * std::max<size_t>(8, (dicCount + 1) / 2);
// 1. no space for a full rewrite, double it
// 2. or space is not large enough for future usage, double it to avoid frequently full rewrite
if (lenNeeded >= fileSize || (lenNeeded + futureUsage) >= fileSize) {
size_t oldSize = fileSize;
// 如果内存重整后仍不足以写入,则将大小不断乘2直至足够写入,最后通过 mmap 重新映射文件
do {
// double 空间直至足够
fileSize *= 2;
} while (lenNeeded + futureUsage >= fileSize);
// if we can't extend size, rollback to old state
if (!m_file->truncate(fileSize)) {
return false;
}
}
return doFullWriteBack(move(preparedData), nullptr);
}
return true;
}


bool MemoryFile::truncate(size_t size) {
。。。。。。。。
// 重新通过 mmap 映射
auto ret = mmap();
if (!ret) {
doCleanMemoryCache(true);
}
return ret;
}

bool MMKV::doFullWriteBack(pair<MMBuffer, size_t> preparedData, AESCrypt *newCrypter) {
auto ptr = (uint8_t *) m_file->getMemory();
auto totalSize = preparedData.second;
#ifdef MMKV_IOS
auto ret = guardForBackgroundWriting(ptr + Fixed32Size, totalSize);
if (!ret.first) {
return false;
}
#endif

#ifndef MMKV_DISABLE_CRYPT
uint8_t newIV[AES_KEY_LEN];
auto decrypter = m_crypter;
auto encrypter = (newCrypter == InvalidCryptPtr) ? nullptr : (newCrypter ? newCrypter : m_crypter);
if (encrypter) {
AESCrypt::fillRandomIV(newIV);
encrypter->resetIV(newIV, sizeof(newIV));
}
#endif

delete m_output;
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
memmoveDictionary(*m_dicCrypt, m_output, ptr, decrypter, encrypter, preparedData);
} else {
#else
{
auto encrypter = m_crypter;
#endif
memmoveDictionary(*m_dic, m_output, ptr, encrypter, totalSize);
}

m_actualSize = totalSize;
#ifndef MMKV_DISABLE_CRYPT
if (encrypter) {
recaculateCRCDigestWithIV(newIV);
} else
#endif
{
recaculateCRCDigestWithIV(nullptr);
}
m_hasFullWriteback = true;
// make sure lastConfirmedMetaInfo is saved
sync(MMKV_SYNC);
return true;
}

内存重整步骤如下:

  • 当剩余映射空间不足以写入需要写入的内容,尝试进行内存重整;
  • 内存重整会将文件清空,将 map 中的数据重新写入文件,从而去除冗余数据;
  • 若内存重整后剩余映射空间仍然不足,不断将映射空间 double 直到足够,并用 mmap 重新映射;

    读取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49

    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);
    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();
    #ifndef MMKV_DISABLE_CRYPT
    if (m_crypter) {
    auto itr = m_dicCrypt->find(key);
    if (itr != m_dicCrypt->end()) {
    auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
    return itr->second.toMMBuffer(basePtr, m_crypter);
    }
    } else
    #endif
    {
    auto itr = m_dic->find(key);
    if (itr != m_dic->end()) {
    auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
    return itr->second.toMMBuffer(basePtr);
    }
    }
    MMBuffer nan;
    return nan;
    }

调用了 getDataForKey 方法获取到了 key 对应的 MMBuffer,之后通过 CodedInputData 将数据读出并返回;没有时,返回默认值MMBuffer nan;

remove

1
2
3
4
5
6
7
8
9
10
void MMKV::removeValueForKey(MMKVKey_t key) {
if (isKeyEmpty(key)) {
return;
}
SCOPED_LOCK(m_lock);
SCOPED_LOCK(m_exclusiveProcessLock);
checkLoadData();

removeDataForKey(key);
}

它在数据读入内存的前提下,调用了 removeDataForKey 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

bool MMKV::removeDataForKey(MMKVKey_t key) {
if (isKeyEmpty(key)) {
return false;
}
#ifndef MMKV_DISABLE_CRYPT
if (m_crypter) {
auto itr = m_dicCrypt->find(key);
if (itr != m_dicCrypt->end()) {
m_hasFullWriteback = false;
构造了一条 size 为 null的 MMBuffer
static MMBuffer nan;
# ifdef MMKV_APPLE
auto ret = appendDataWithKey(nan, key, itr->second);
if (ret.first) {
auto oldKey = itr->first;
m_dicCrypt->erase(itr);
[oldKey release];
}
# else
auto ret = appendDataWithKey(nan, key);
if (ret.first) {
m_dicCrypt->erase(itr);
}
# endif
return ret.first;
}
}

return false;
}

这里实际上是构造了一条 size 为null 的 MMBuffer 并调用 appendDataWithKey 将其 append 到 protobuf 文件中,并将 key 对应的内容从 map 中删除;读取时发现它的 size 为 null,则会认为这条数据已经删除;

总结

MMKV

  • 1.protobuf文件存储,文件更小;
  • 2.采用mmap,文件拷贝速度更快;
  • 3.不阻塞主线程,采用缺页方式,避免数据丢失。
  • 4.支持增量更新,不管key是否重复,直接将数据加载前数据后。
  • 5.文件大小不够,就需要全量写入:但是需要首先去重,去重后,如果空间够,就把数据编码成mmkv文件格式,全量的覆盖写入文件,如果不够,就需要先扩容(*2);
  • 6.扩容,设置文件大小*2,通过unmap,解除映射;重新映射mmap(size * 2),这里是个do while循环判断;
  • 7.如果文件损坏:回调给开发者或者默认直接丢失数据,重新保存数据。
  • 8.支持多进程:文件锁flock。多进程数据同步:crc32校验

数据对比

参考资料

关于SharePreference使用以及内部原理简单解析
庖丁解牛之SharedPreferences超级大卡顿
MMKV
https://www.51cto.com/article/686452.html