对于 Android 轻量级存储方案,SharedPreferences、MMKV和DataStore都是用来进行键值对存储的,那么在项目中该如何选用呢?
是 Android 中简单易用的轻量级存储方案,用来保存相关信息。
以键值对(key-value)的方式保存数据到 xml 文件
文件路径为:/sdcard/
data/data/应用程序包名/shared_prefs
适合存储一些简单的数据,提供了多种数据类型的存储,包括:int、long、string、Boolean、float、Set
读取数据:
通过解析 xml 文件,得到指定 key 对应的 value不支持多进程,虽然有基于 ContentProvider 封装的实现,但是性能低下,经常导致 ANR
SharedPreferences 是个接口,其真正实现类是 SharedPreferencesImpl。
每当调用 SharedPreferencesImpl 的构造器的时候,都会开始调用 startLoadFromDisk 方法,然后在该方法中开启一个子线程加载 xml 文件中的内容,最后将 xml 中的内容全部加载到 mMap中
当 xml 中数据过大时,将xml一次性加载到内存中,就会导致内存占用过高,可能出现OOM
在获取之前使用了synchronized同步锁,如果 sp 还没加载完毕,主线程会一直阻塞在那里,直到加载 sp 的子线程加载完成
当数据很大时,可能会导致卡顿。
每次调用 edit 方法都会创建一个 Editor 对象,造成额外的内存占用
调用频繁的场景,在多次 put 之后再统一进行
commit/apply,
也就是一次更新多个键值对,只进行一次 IO 操作commit/apply 引发的ANR问题
- commit 是同步地提交到硬件磁盘,如果在主线程中提交会阻塞线程,影响后续的操作,可能导致 ANR
- apply 是将修改数据提交到内存,然后异步提交到硬件磁盘,没有返回值,异步操作不会阻塞调用的线程,但是如果写入任务比较耗时,会阻塞住主线程
commit真正的实现类是SharedPreferencesImpl,在这个类中的commit方法中。
通过enqueueDiskWrite进行写入任务,然后阻塞调用commit的线程,让调用线程处于等待状态,等写入任务完成之后就唤起调用commit的线程,如果实在主线程中执行的commit,就会阻塞主线程,如果写入任务很多,比较耗时,阻塞时间过长,可能导致ANR。
所以不要在主线程执行写入文件的操作
在apply方法中,通过enqueueDiskWrite进行写入任务,但是它最后一个参数不为空,是一个postWriteRunnable回调,因此这里是一个异步任务,这个异步任务的执行是在QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)中
QueuedWork:
QueuedWork是Android系统提供的一个执行异步任务的工具类
内部的实现逻辑的就是创建一个HandlerThread作为工作线程,然后QueuedWorkHandler和这个HandlerThread进行管理,每当有任务就添加进来,在这个异步线程中执行,线程名为:”queued-work-looper“
每次调用queue方法,都忘sWork 添加一个任务,它是一个LinkedList,这个队列中数据最终在queued-work-looper 线程中依次得到执行
apply写入操作是在异步线程中执行,不会导致主线程卡顿
但是如果异步任务执行时间过长,当ActvityThread执行了handleStopActivity()、handleServiceArgs()、handlePauseActivity() 等方法的时候都会调用QueuedWork.waitToFinish(),而此方法中会在异步任务执行完成前一直阻塞主线程,造成卡顿
也就是说虽然apply写入操作是在异步线程中执行,但是如果要销毁的时候就会等待异步线程执行完成,导致卡顿
数据加载是异步执行的,开启了一个子线程去执行,但是在get数据的时候,会调用awaitLoadedLocked 这个方法,当数据没有加载完,就让调用的线程处于等待中,导致阻塞
MMKV是微信开源出来的,起初是为了在文字显示之前先把它记录到磁盘,如果文字显示失败导致程序崩溃,就可以将它从磁盘里恢复
基于内存映射文件的方式实现的
使用mmap方式进行内存映射的key-value组件,底层使用protobuf实现,通过将文件映射到内存中,从而使得读取和写入操作都可以在内存中进行,避免了频繁的IO操作,写入性能是极高的
DataStore 被创造出来的目标就是替代 SharedPreferences