再见 SharedPreferences,你好 MMKV!,大厂面试必备技能
不过也带来很多问题,尤其是由 SP 引起的 ANR 问题,非常常见。
正因如此,后来也出现了一些 SP 的替代解决方案,比如 MMKV。
本文主要包括以下内容
SharedPreferences 存在的问题
MMKV 的基本使用与介绍
MMKV 的原理
==================================================================================
SP 的效率比较低
读写方式:直接 I/O
数据格式:xml
写入方式:全量更新

由于 SP 使用的 xml 格式保存数据,所以每次更新数据只能全量替换更新数据。
这意味着如果我们有 100 个数据,如果只更新一项数据,也需要将所有数据转化成 xml 格式,然后再通过 io 写入文件中。
这也导致 SP 的写入效率比较低。
commit 导致的 ANR
public?boolean?commit()?{
//?在当前线程将数据保存到 mMap 中
MemoryCommitResult?mcr?=?commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr,?null);
try?{
//?如果是在 singleThreadPool 中执行写入操作,通过 await()暂停主线程,直到写入操作完成。
// commit 的同步性就是通过这里完成的。
mcr.writtenToDiskLatch.await();
}?catch?(InterruptedException?e)?{
return?false;
}
/*
*?回调的时机:
*?1.?commit 是在内存和硬盘操作均结束时回调
*?2.?apply 是内存操作结束时就进行回调
*/
notifyListeners(mcr);
return?mcr.writeToDiskResult;
}
如上所示
commit 有返回值,表示修改是否提交成功。
commit 提交是同步的,直到磁盘操作成功后才会完成。
所以当数据量比较大时,使用 commit 很可能引起 ANR。
Apply 导致的 ANR
commit 是同步的,同时 SP 也提供了异步的 apply。
apply 是将修改数据原子提交到内存, 而后异步真正提交到硬件磁盘, 而 commit 是同步的提交到硬件磁盘,因此,在多个并发的提交 commit 的时候,他们会等待正在处理的 commit 保存到磁盘后在操作,从而降低了效率。
而 apply 只是原子的提交到内容,后面有调用 apply 的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。
但是 apply 同样会引起 ANR 的问题。
public?void?apply()?{
final?long?startTime?=?System.currentTimeMillis();
final?MemoryCommitResult?mcr?=?commitToMemory();
final?Runnable?awaitCommit?=?new?Runnable()?{
@Override
public?void?run()?{
mcr.writtenToDiskLatch.await();?//?等待
......
}
};
//?将?awaitCommit?添加到队列?QueuedWork?中
QueuedWork.addFinisher(awaitCommit);
Runnable?postWriteRunnable?=?new?Runnable()?{
@Override
public?void?run()?{
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr,?postWriteRunnable);
}
将一个?awaitCommit?的?Runnable?任务,添加到队列?QueuedWork?中,在 awaitCommit 中会调用?await()?方法等待,在?handleStopService?、handleStopActivity?等等生命周期会以这个作为判断条件,等待任务执行完毕。
将一个?postWriteRunnable?的?Runnable?写任务,通过 enqueueDiskWrite?方法,将写入任务加入到队列中,而写入任务在一个线程中执行。
为了保证异步任务及时完成,当生命周期处于?handleStopService()?、handlePauseActivity()?、?handleStopActivity()?的时候会调用 QueuedWork.waitToFinish()?会等待写入任务执行完毕。
private?static?final?ConcurrentLinkedQueue<Runnable>?sPendingWorkFinishers?=
new?ConcurrentLinkedQueue<Runnable>();
public?static?void?waitToFinish()?{
Runnable?toFinish;
while?((toFinish?=?sPendingWorkFinishers.poll())?!=?null)?{
toFinish.run();?//?相当于调用?mcr.writtenToDiskLatch.await()
?方法
}
}
sPendingWorkFinishers?是?ConcurrentLinkedQueue?实例,apply?方法会将写入任务添加到?sPendingWorkFinishers 队列中,在单个线程的线程池中执行写入任务,线程的调度并不由程序来控制,也就是说当生命周期切换的时候,任务不一定处于执行状态。
toFinish.run()?方法,相当于调用?mcr.writtenToDiskLatch.await()?方法,会一直等待。
waitToFinish()?方法就做了一件事,会一直等待写入任务执行完毕,其它什么都不做,当有很多写入任务,会依次执行,当文件很大时,效率很低,造成 ANR 就不奇怪了。
所以当数据量比较大时,apply 也会造成 ANR。
getXXX() 导致 ANR
不仅是写入操作,所有?getXXX()?方法都是同步的,在主线程调用?get?方法,必须等待 SP 加载完毕,也有可能导致 ANR。 调用?getSharedPreferences()?方法,最终会调用 SharedPreferencesImpl#startLoadFromDisk()?方法开启一个线程异步读取数据。
private?final?Object?mLock?=?new?Object();
private?boolean?mLoaded?=?false;
private?void?startLoadFromDisk()?{
synchronized?(mLock)?{
mLoaded?=?false;
}
new?Thread("SharedPreferencesImpl-load")?{
public?void?run()?{
loadFromDisk();
}
《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
}.start();
}
正如你所看到的,开启一个线程异步读取数据,当我们正在读取一个比较大的数据,还没读取完,接着调用?getXXX()?方法。
public?String?getString(String?key,?@Nullable?String?defValue)?{
synchronized?(mLock)?{
awaitLoadedLocked();
String?v?=?(String)mMap.get(key);
return?v?!=?null???v?:?defValue;
}
}
private?void?awaitLoadedLocked()?{
......
while?(!mLoaded)?{
try?{
mLock.wait();
}?catch?(InterruptedException?unused)?{
}
}
......
}
在同步方法内调用了?wait()?方法,会一直等待?getSharedPreferences()?方法开启的线程读取完数据才能继续往下执行,如果读取几 KB 的数据还好,假设读取一个大的文件,势必会造成主线程阻塞。
===================================================================
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Win32 / POSIX 平台,一并开源。
MMKV 优点
MMKV 实现了 SharedPreferences 接口,可以无缝切换。
通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心?crash?导致数据丢失。
MMKV 数据序列化方面选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
SP 是全量更新,MMKV 是增量更新,有性能优势。
详细的使用细节可以参考文档:https://github.com/Tencent/MMKV/wiki
==================================================================
IO 操作
我们知道,SP 是写入是基于 IO 操作的,为了了解 IO,我们需要先了解下用户空间与内核空间
虚拟内存被操作系统划分成两块:用户空间和内核空间,用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。

最后
下面是有几位 Android 行业大佬对应上方技术点整理的一些进阶资料。希望能够帮助到大家提升技术

高级 UI,自定义 View
UI 这块知识是现今使用者最多的。当年火爆一时的 Android 入门培训,学会这小块知识就能随便找到不错的工作了。
不过很显然现在远远不够了,拒绝无休止的 CV,亲自去项目实战,读源码,研究原理吧!

评论