锦囊篇|一文摸懂SharedPreferences和MMKV

使用方法

SharedPreferences

// 1:得到SharedPreferences,这是直接包含在Context中的方式,直接调用便可
// 四种写入模式:MODE_PRIVATE、MODE_APPEND、MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE
val sp = baseContext.getSharedPreferences("clericyi", Context.MODE_PRIVATE)
// 2:获取笔,由于第一步得到到至关于一张白纸,须要对应的笔才能对其操做
val editor = sp.edit()
// 3:数据操做,不过咱们当前操做的数据只是一个副本
// putString()、putInt()。。。还有不少方法
editor.putBoolean("is_wirte", true)
// 4:两种提交方式,将副本内的数据正式写入实体文件中
editor.commit() // 同步写入
editor.apply() // 异步写入

MMKV

第一步:开源库导入android

implementation 'com.tencent:mmkv-static:1.1.2'

第二步:使用git

// 1. 自定义Aapplication
public void onCreate() {
super.onCreate();
MMKV.initialize(this);
}

// 2. 调度使用
// 和SharedPreferenced同样,支持的数据类型直接往里面塞便可
// 不同的地方,MMKV不须要本身去作一些apply()或者是commit()的操做,更加方便
MMKV kv = MMKV.defaultMMKV();

kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");

为何这篇文章要拿两个框架讲?

单进程性能github

多进程性能web

不管是单线程仍是多线程,MMKV的读写能力都远远的甩开了SharedPreferences&SQLite&SQLite+Transacion,可是MMKV究竟是如何作到如此快的进行读写操做的?这就是下面会经过源码分析完成的事情了。安全

另外接下来的一句话仅表明了个人我的意见,也是为何我只写SharedPreferencesMMKV二者比较的缘由,由于我我的认为SQLite和他们不太属于同一类产品,因此比较的意义上来讲就趋于普通。微信

SharedPreferences源码分析

根据上述中所说起过的使用代码,可以比较清楚的知道第一步的分析对象就是getSharedPreferences()的获取操做了,可是若是你直接点进去搜这个方法,是否是会出现这样的结果呢?多线程

public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);

没错了,只是一个抽象方法,那显然如今最重要的事情就是找到他的具体实现类是什么了,固然你能够直接查阅资料获取,最后的正确答案就是ContextImpl,不知道你有没有找对呢?并发

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) {
// .....
// 经过具体实现类,对SharedPreferences进行建立
sp = new SharedPreferencesImpl(file, mode);
// 经过一个cache来防止同一个文件的SharedPreferences的重复建立
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// 若是开启了多进程模式,一旦数据发生更新,那么其余进程的数据会经过重载的方式更新
// 这里是否存在疑问,为何网上会说这个方法是一个进程不安全的方案呢?
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}

在上面的使用过程当中提到了,其余他是一个副本的概念,这个从何提及呢?显然这就要看一下SharedPreferences的实现类具体是如何进行操做的了,从他的构造函数看起,慢慢进入深度调用。app

SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
// 开始从磁盘中调数据
startLoadFromDisk(); // 1 -->
}
// 1 -->
private void startLoadFromDisk() {
// 开启一条新的线程来加载数据
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk(); // 2-->
}
}.start();
}
// 2 -->
private void loadFromDisk() {
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
// 从XML中把数据读出来,并把数据转化成Map类型
// 这是一个很是消耗时间的操做
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
}

synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
if (thrown == null) {
// 文件里拿到的数据为空就重建,存在就赋值
if (map != null) {
// 将数据存储放置到具体类的一个全局变量中
// 稍微记一下这个关键点
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
}
}

到此为止就基本完成了SharedPreferences的构建流程,而为了可以对数据进行操做,那就须要去获取一只笔,来进行操做,一样的这段代码最后会在SharedPreferencesImpl中进行具体实现。框架

public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
// 很简单的一个操做,就是建立了一支笔
// 这里是一个很重要的点,由于每次都是新建立一支笔,因此要作到数据更换的操做要一次性完成。
return new EditorImpl();
}

由于后面的操做都是与这只笔相关,并且具体操做上重复度比较高,因此只选取一个putString()来进行分析。

public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = new HashMap<>();

public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
}

很简单的解决思路,就是新建立一个了HashMap里面所有保存的都是一些咱们已经作过修改的数据,以后的更新是须要用到这些数据的。

相较于以前的那些源码,这里的就显得很是轻松了,结合上述的源码分析,能够假设SharedPreferences氛围三个要点。

  1. mMap: 存储从文件中拉取的数据。
  2. mModified: 存储但愿修改值的数据。
  3. apply()/commit(): 猜想最后就是上述二者数据的合并,再进行数据提交。

数据提交

异步提交 / apply()

public void apply() {
// .....
// 这一步其实就是咱们所猜想的第三步中的数据合并
// 作一个简单的介绍,数据的替换一共分为三步:
// 1. 将数据存储到mapToWriteToDisk中
// 2. 与mModified中数据进行比较,不存在或者不一致就替换
// 3. 将更新后得数据返回
final MemoryCommitResult mcr = commitToMemory();
// 经过CountDownLatch来完成数据的同步更新
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
mcr.writtenToDiskLatch.await(); // 1-->
}
};
// 对事件完成的监听
QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
// 经过线程进行异步处理
awaitCommit.run();
// 若是任务完成,就从队列中清除
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // 2 -->
// 通知观察者数据更新
notifyListeners(mcr);
}

从上述的代码中能够了解到apply()的经过建立一个线程来进行处理,以后会讲到commit()和他的处理方式不一样的地方。如今具体的目光仍是要聚焦在如何完成数据到磁盘的提交的,也就是注释1处的具体实现究竟是如何?这就是对这个类的一个理解问题了。其实他有点相似于程序计数器,在阻塞数量大于线程数时,会阻塞运行,而超出数量就会出现并发情况。

第二个地方就是注释2,他线程作了一个入队列的操做。

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);
}
}
};
// commit()一样会进入到这个大方法中
// commit()方法执行到这里运行完就结束,干的事情就是将数据写入文件
if (isFromSyncCommit) {
writeToDiskRunnable.run();
return;
}
// apply()多作了层如队列的操做,意图在于异步进行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); // 3-->
}
// 3 -->
// 由于最后使用的都是其实都是MSG_RUN的参数,因此直接调用查看便可
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); // 4-->
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); // 4-->
}
}
}
// 4 -->
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork(); // 5 -->
}
}
// 5 -->
// 就是最后将一个个任务进行完成运行
private static void processPendingWork() {
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
// 。。。。。
if (work.size() > 0) {
for (Runnable w : work) {
w.run(); // 最后将数据一个个进行运行完成操做
}
}
}
}

同步提交 / commit()

public boolean commit() {

MemoryCommitResult mcr = commitToMemory();
// 不须要使用线程来进行异步处理,因此第二参数为空
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);

mcr.writtenToDiskLatch.await();

notifyListeners(mcr);
return mcr.writeToDiskResult;
}

因此说基本逻辑上其实仍是和apply()方法是一致的,只是去除了异步处理的步骤,因此就是常说的同步处理方式。

总结

  1. 是什么限制了SharedPreferences的处理速度?

这个问题在上面的源码分析中其实已经有所说起了,那就是文件读写,因此如何加快文件的读写速度是一个相当重要的突破点。固然向速度妥协的一个方案,想来你也已经看到了,那就是异步提交,经过子线程的在用户无感知的状况下把数据写到文件中。

  1. 为何多线程安全,而多进程不安全的操做?

多线程安全安全想来是一个很是容易解释的事情了,干一个很简单的事情就是synchronized的加锁操做,对数据的操做进行加锁那势必拿到的最后数据就会是一个安全的数据了。

可是对于多进程呢? 你可能会说在sp.startReloadIfChangedUnexpectedly();这段代码出现的难道不是已经涉及了多进程的安全操做吗?yep!! 若是你想到了这点,说明你有好好看了下代码,可是没有看他的实现,若是你去看他的实现方案,就会发现MODE_MULTI_PROCESS和所可使用的操做的运算结果均为0,因此在如今的Android版本中这是一个被抛弃的方案。固然这是其一,天然还有另一个判断就是关于版本方面,若是小于HONEYCOMB一样能够进入这个方案,可是须要注意getSharedPreferences()是只有获取时才会出现的,而SharedPreferences是对于单进程而言的单独实例,数据的备份所有在单个进程完成,因此在进行多进程读写时,发生错误是大几率的。

MMKV源码分析

初始化 / MMKV.initialize(this);

MMKV的整套流程中,MMKV的初始化起着承上启下的做用。

public static String initialize(Context context) {
// 获取根路径
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
return initialize(root, (MMKV.LibLoader)null, logLevel); // 进行加载 1 -->
}
// 1 -->
public static String initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel) {
// 加载必要的so文件
// 。。。。。
// 经过JNI来对底层c的实现进行初始化调度
jniInitialize(MMKV.rootDir, logLevel2Int(logLevel));
return rootDir;
}

由于到这里的话直接经过三方库的导入已经不能知足查看了,因此直接去下载MMKV的开源库源码查看比较合适。

若是你并不太熟悉JNI的方法调度,也不要紧,我会慢慢的经过方式来教你入门。

你可以发现是爆红的JNI方法,那如何定位呢? 摁两下Shift的全局搜索,而后直接输入initializeMMKV,就会获得搜索结果了。

可以发现这里存在两个方法,进去看看就知道像C写的,那目标群体就已经被你锁定了。

void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
// ThreadOnce说明初始化过程只会进行一次
ThreadLock::ThreadOnce(&once_control, initialize);

g_rootDir = rootDir;
// 对目标路径进行设置,层级递推若是不存在就建立。
mkPath(g_rootDir);
}

对象实例获取 / MMKV.defaultMMKV()

public static MMKV defaultMMKV() {
// 能够设置为多进程模式
// 重点所在
long handle = getDefaultMMKV(SINGLE_PROCESS_MODE, null); // *** -->
return new MMKV(handle); // 1 -->
}
// 1 -->
// 就是一个为long类型的handle变量设置
private MMKV(long handle) {
nativeHandle = handle;
}

你能看到***注释位置是代码中一个迷惑性行为,经过数据类型定义可以知道最后获得的数据是一个数据类型为long的数据,咱们能够猜想这个数据的用处对应着最后可以用于寻找到对应的MMKV,经过深层次调用后能够发现他调用了一个mmkvWithID()的方法,其中DEFAULT_MMAP_IDmmkv.default

MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {
// 。。。。。。
auto mmapKey = mmapedKVKey(mmapID, relativePath); // 1 -->
auto itr = g_instanceDic->find(mmapKey);
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second; // 2 -->
return kv;
}
if (relativePath) {
if (!isFileExist(*relativePath)) {
if (!mkPath(*relativePath)) {
return nullptr;
}
}
}
auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath); // 3 -->
(*g_instanceDic)[mmapKey] = kv;
return kv;
}

在这个代码中总共有两个核心部分:

  1. mmapKey的值的计算: 经过 mmapIDrelativePath两个值进行必定的运算操做,具体关系就是 mmapIDrelativePath的重合关系,具体仍是要见于代码实现。
  2. MMKV的生成: 这里的解释对应 注释2注释3,就是经过一个 Map的形式来对数据进行存储,若是在 g_instanceDic这个变量中进行数据查询。

MMKV的内部结构

MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath)
: m_mmapID(mmapedKVKey(mmapID, relativePath)) // historically Android mistakenly use mmapKey as mmapID
, m_path(mappedKVPathWithID(m_mmapID, mode, relativePath)) // 1 -->
, m_crcPath(crcPathWithID(m_mmapID, mode, relativePath))
, m_dic(nullptr)
, m_dicCrypt(nullptr)
, m_file(new MemoryFile(m_path, size, (mode & MMKV_ASHMEM) ? MMFILE_TYPE_ASHMEM : MMFILE_TYPE_FILE))
, m_metaFile(new MemoryFile(m_crcPath, DEFAULT_MMAP_SIZE, m_file->m_fileType))
, m_metaInfo(new MMKVMetaInfo())
, m_crypter(nullptr)
, m_lock(new ThreadLock())
, m_fileLock(new FileLock(m_metaFile->getFd(), (mode & MMKV_ASHMEM)))
, m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
, m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
, m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0 || (mode & CONTEXT_MODE_MULTI_PROCESS) != 0) {
m_actualSize = 0;
m_output = nullptr;

if (cryptKey && cryptKey->length() > 0) {
m_dicCrypt = new MMKVMapCrypt();
m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
} else {
m_dic = new MMKVMap();
}
// 。。。。。。一些赋值操做

// sensitive zone
{
SCOPED_LOCK(m_sharedProcessLock);
loadFromFile();
}
}

SharedPreferences相同最后仍是须要经历一场和文件读写的殊死搏斗,那问题就来了,一样是文件读写,为何MMKV可以以百倍的速度碾压各种已成熟的产品呢?从个人思路出发能够分为这样的几种状况:

  1. 不够健壮的错误数据处理。 这若是你作一个简易版的 FastJson就可以发现,数据的处理速度基本上可以有很是高的提高。可是这对于相对成熟的产品而言通常不会有这种方案。
  2. 底层进行数据处理。 这个方案的推行在必定程度上也是对应如今的二者对比有必定的道理,由于可以发现 MMKV的实现方案基本都是依靠 JNI来调度完成,而 C的处理速度和 Java相比想来咱们也是有目共睹的。
  3. 更优化的文件读取方案。 这就是对当前方案的分析了,由于尚未看到后面的代码,因此这里是一种方案的猜想。由于 SharedPreferencesMMKV二者都是咱们有目共睹须要对数据进行读写操做的,而数据的最后来源就是本地的文件,一个更易于读写的文件方案势必是一个最关键的突破点。
  4. 。。。。。接下来由你开始进行更多的思考。

回归正题:loadFromFile();

在刚刚的猜测中,我说起了关于文件读写的问题,由于对MMKV而言,文件读写这一关确定是躲不过去的,可是如何更高效就是咱们应该去思考的点了。

void MMKV::loadFromFile() {
// 文件不合法就从新加载
if (!m_file->isFileValid()) {
m_file->reloadFromFile();
}
// 文件依旧不合法就报错
if (!m_file->isFileValid()) {
MMKVError("file [%s] not valid", m_path.c_str());
} else {
// 进入这一步至少说明文件是合法的,可是须要进行数据的校验
// error checking
bool loadFromFile = false, needFullWriteback = false;
checkDataValid(loadFromFile, needFullWriteback);
auto ptr = (uint8_t *) m_file->getMemory();
// loading
if (loadFromFile && m_actualSize > 0) {
MMBuffer inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
if (m_crypter) {
clearDictionary(m_dicCrypt);
} else {
clearDictionary(m_dic);
}
// 1 -->
if (needFullWriteback) {
if (m_crypter) {
MiniPBCoder::greedyDecodeMap(*m_dicCrypt, inputBuffer, m_crypter); // 2 -->
} else {
MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer); // 2 -->
}
} else {
// 1 -->
if (m_crypter) {
MiniPBCoder::decodeMap(*m_dicCrypt, inputBuffer, m_crypter); // 2 -->
} else {
MiniPBCoder::decodeMap(*m_dic, inputBuffer); // 2 -->
}
}
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
m_output->seek(m_actualSize); // 计算出数据量的实际大小
if (needFullWriteback) {
fullWriteback();
}
} else {
// 若是数据是不合法或者空的就直接丢弃。
// 。。。。。。
}
}

m_needLoadFromFile = false;
}

在代码段中我标注出了注释1注释2,也是我认为相当重要的代码了,分别作了两大操做:

  1. 数据的写回方案制做: 这是要一个很是有特点的地方,为何这么说呢?其实你可以从一个判断的变量名可以看出会对数据的写回方式有一个选择,也就是部分写回和所有写回的策略之选,那这就是第一个缘由为何 MMKV的综合性能可以强过 SharedPreferences
  2. 文件格式的选择: 其实这是解析时候的事情了,这一段的论证来源于 MMKV 原理 [1]protobuf做为 MMKV最后的选择方案在性能和空间占用上都有不错的表现。

数据更新 / kv.encodeXXX("string", XXX);

这里的代码分析只拿一个做为样例便可

MMKV_JNI jboolean encodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean value) {
MMKV *kv = reinterpret_cast<MMKV *>(handle); // 1-->
if (kv && oKey) {
string key = jstring2string(env, oKey); // 将key再进行特殊的加工处理
return (jboolean) kv->set((bool) value, key); // 2 -->
}
return (jboolean) false;
}

关注几个注释点:

  1. 注释1: 这就是以前在上面的时候已经提到过的在 Java这一层中进行的操做只是一个数据类型为 longhandle变量进行赋值操做,而这个 handle中在后期能够被解析转化为已经初始化完成的 MMKV对象。
  2. 注释2: 完成相对应的数据放置操做,那这里就要观察代码的深层调度是一个怎么样的过程了。
bool MMKV::set(bool value, MMKVKey_t key) {
// 1. 进行数据的测量,并建立相同大小的区间
size_t size = pbBoolSize();
MMBuffer data(size);
// 2. 转化为CodedOutputData对象用于写入
CodedOutputData output(data.getPtr(), size);
output.writeBool(value); // 3 -->
// 从名字就能知道这实际上是一个正式的数据替换操做
// 追溯后能够发现会出现一个文件的写入。
return setDataForKey(move(data), key);
}
// 3-->
void CodedOutputData::writeBool(bool value) {
// 用0和1来表示最后的数值
this->writeRawByte(static_cast<uint8_t>(value ? 1 : 0));
}

可是经过官方的文档中可以知道,关于这个文件格式下的数据是存在问题的,那就是他并不支持增量更新 ,这也就意味着复杂的操做会更加多了,那腾讯的解决方案是什么呢?

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

一句话讲来就是,新的或更改过的就最后新增后面插入。

而新旧数据累加势必会形成文件的庞大,那这方面MMKV给出的解决方案又是怎么样的呢?

之内存 pagesize 为单位申请空间,在空间用尽以前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间仍是不够用的话,将文件扩大一倍,直到空间足够。

一样的换成一句话来进行描述,有上限目标的文件重写。

这一段的代码实现就不贴出了,具体位置就在MMKV_IO中的ensureMemorySize()方法,经过已存在数据大小的总量来进行整理,由于不少时候数据量很大是由于大容量的数据的重复添加形成的。

数据获取 / kv.decodeXXX("string");

MMKV_JNI jboolean decodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean defaultValue) {
MMKV *kv = reinterpret_cast<MMKV *>(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->getBool(key, defaultValue);
}
return defaultValue;
}

其实基本逻辑和写文件的差很少了,这个时候仍是首先要获取一个对应的MMKV对象,而后完成数据的获取。

bool MMKV::getBool(MMKVKey_t key, bool defaultValue) {
auto data = getDataForKey(key);
if (data.length() > 0) {
CodedInputData input(data.getPtr(), data.length());
return input.readBool();
}
return defaultValue;
}

转化为CodedInputData的对象来完成数据的读取,若是数据不存在,那就直接默认值返回。

删除对应的数据 / kv.removeValueForKey("string")

在看代码以前作一个思考,在已知的数据基础上,换成你会怎么作这样的操做呢?

咱们要关注的点有如下几个:

  1. protobuf是一个不支持增量更新的文件格式,相对应 MMKV给出的解决方案就是经过尾部增长,出现新旧数据叠加
  2. 问题1的引伸,新旧数据叠加的一个查询和删除问题,由于新旧数据,那么作查询的时候势必要屡次的查,若是每次的数据都有 1G,那你的查询每次都要叠加到 1G的程度,而不是查到便可开始删除。

对于以上问题思考清楚了的话,咱们就能够给出MMKV的解决方案了。

auto itr = m_dic->find(key);
if (itr != m_dic->end()) {
m_hasFullWriteback = false;
static MMBuffer nan; // ******
auto ret = appendDataWithKey(nan, itr->second); // ******
if (ret.first) {
#ifdef MMKV_APPLE
[itr->first release];
#endif
m_dic->erase(itr);
}
return ret.first;
}

将关注点所有放置于注释带*的代码段上,一个没有赋值的MMBuffer说明数据为空,而后直接调用appendDataWithKey()文件写入的方案,说明最后出如今protobuf的数据样式会是这样的。

message empty{

}

其实就是往里面加一个新的空数据做为新的数据。

总结

从源码分析完以后,和SharedPreferences相比,从新整理后能够总结为如下几点的突破:

  1. mmap的使用: 内存映射的技术的使用,减小了 SharedPreferences 的拷贝和提交的时间消耗。
  2. 数据的更新方式: 局部更新的数据,经过尾部追加来进行完成,而不是像 SharedPreferences同样的直接文件重构。一样要注意这样的方式会形成冗余数据的增长。
  3. 多进程访问安全的设计: 详细见于 MMKV for Android 多进程设计与实现 [2],主要仍是以 mmap做为突破口,来完成对其余进程对当前文件的操做的一个状态感知,主要就是分为三方面: 写指针增加、内存重整、内存增加

参考资料

  1. MMKV官方文档 [3]

参考资料

[1]

MMKV 原理: https://github.com/Tencent/MMKV/wiki/design#mmkv-原理

[2]

MMKV for Android 多进程设计与实现: https://github.com/Tencent/MMKV/wiki/android_ipc

[3]

MMKV官方文档: https://github.com/Tencent/MMKV/wiki


本文分享自微信公众号 - 告物(ClericYi_Android)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索