共计 4695 个字符,预计需要花费 12 分钟才能阅读完成。
前言
SharedPreferences 是谷歌提供的轻量级存储计划,应用起来比拟不便,能够间接进行数据存储,不用另起线程。
不过也带来很多问题,尤其是由 SP 引起的 ANR 问题,十分常见。
正因如此,起初也呈现了一些 SP 的代替解决方案,比方 MMKV。
本文次要包含以下内容
1.SharedPreferences 存在的问题
2.MMKV 的根本应用与介绍
3.MMKV 的原理
SharedPreferences 存在的问题
SP 的效率比拟低
1. 读写形式:间接 I /O
2. 数据格式:xml
3. 写入形式:全量更新
因为 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;
}
如上所示
1.commit 有返回值,示意批改是否提交胜利。
2.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();
}
}.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 的应用
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化 / 反序列化应用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上应用,其性能和稳定性通过了工夫的验证。近期也已移植到 Android / macOS / Win32 / POSIX 平台,一并开源。
MMKV 长处
1.MMKV 实现了 SharedPreferences 接口,能够无缝切换。
2. 通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不用放心 crash 导致数据失落。
3.MMKV 数据序列化方面选用 protobuf 协定,pb 在性能和空间占用上都有不错的体现。
4.SP 是全量更新,MMKV 是增量更新,有性能劣势。
具体的应用细节能够参考文档:https://github.com/Tencent/MM…
MMKV 原理
为什么 MMKV 写入速度更快
IO 操作
咱们晓得,SP 是写入是基于 IO 操作的,为了理解 IO,咱们须要先理解下用户空间与内核空间
虚拟内存被操作系统划分成两块:用户空间和内核空间,用户空间是用户程序代码运行的中央,内核空间是内核代码运行的中央。为了平安,它们是隔离的,即便用户的程序解体了,内核也不受影响。
写文件流程:
1、调用 write,通知内核须要写入数据的开始地址与长度。
2、内核将数据拷贝到内核缓存。
3、由操作系统调用,将数据拷贝到磁盘,实现写入。
MMAP
Linux 通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。
对文件进行 mmap,会在过程的虚拟内存调配地址空间,创立映射关系。
实现这样的映射关系后,就能够采纳指针的形式读写操作这一段内存,而零碎会主动回写到对应的文件磁盘上
MMAP 劣势
1、MMAP 对文件的读写操作只须要从磁盘到用户主存的一次数据拷贝过程,缩小了数据的拷贝次数,进步了文件读写效率。
2、MMAP 应用逻辑内存对磁盘文件进行映射,操作内存就相当于操作文件,不须要开启线程,操作 MMAP 的速度和操作内存的速度一样快。
3、MMAP 提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统如内存不足、过程退出等时候负责将内存回写到文件,不用放心 crash 导致数据失落。
能够看出,MMAP 的写入速度根本与内存写入速度统一,远高于 SP,这就是 MMKV 写入速度更快的起因。
MMKV 写入形式
SP 的数据结构
SP 是应用 XML 格局存储数据的,如下所示。
然而这也导致 SP 如果要更新数据的话,只能全量更新。
MMKV 数据结构
MMKV 数据结构如下
MMKV 应用 Protobuf 存储数据,冗余数据更少,更省空间,同时能够不便地在开端追加数据。
写入形式
增量写入
不论 key 是否反复,间接将数据追加在前数据后。这样效率更高,更新数据只须要插入一条数据即可。
当然这样也会带来问题,如果一直增量追加内容,文件越来越大,怎么办?
当文件大小不够,这时候须要全量写入。将数据去掉反复 key 后,如果文件大小满足写入的数据大小,则能够间接更新全量写入,否则须要扩容。(在扩容时依据均匀每个 K - V 大小计算将来可能须要的文件大小进行扩容,避免经常性的全量写入)
MMKV 三大劣势
- mmap 避免数据失落,进步读写效率;
- 精简数据,以起码的数据量示意最多的信息,缩小数据大小;
- 增量更新,防止每次进行绝对增量来说大数据量的全量写入。
我的库存,须要的小伙伴请点击我的 GitHub 收费支付