Android轻量级存储SharedPreferences MMKV Jetpack DataStore方案
作者:如愿以偿丶
SharePreferences使用
SharedPreferences:一个轻量级的存储类,特别适合用于保存应用配置参数。(是用xml文件存放数据,文件存放在/data/data/<package name>/shared_prefs目录下)
SharedPreferences用法:
1.保存数据
保存数据一般分为以下步骤:
使用Activity类的getSharedPreferences方法获得SharedPreferences对象;
使用SharedPreferences接口的edit获得SharedPreferences.Editor对象;
通过SharedPreferences.Editor接口的putXXX方法保存key-value对;
通过过SharedPreferences.Editor接口的commit方法保存key-value对。
2.读取数据
使用Activity类的getSharedPreferences方法获得SharedPreferences对象;
通过SharedPreferences对象的getXXX方法获取数据;
3.示例
//-------------------- SharePreferences ------------------------- //获取SharePreferences private val sp = context.applicationContext.getSharedPreferences(BOOK_PREFERENCES_NAME, MODE_PRIVATE) /** * SharePreferences 存数据 */ fun saveBookSP(book: BookBean) { //commit默认为false,采用异步提交。 sp.edit(commit = true) { putString(KEY_BOOK_NAME, book.name) putFloat(KEY_BOOK_PRICE, book.price) putString(KEY_BOOK_TYPE, book.type.name) } } /** * SharePreferences 获取数据 */ val mBookInfo: BookBean get() { sp.apply { var bookName = getString(KEY_BOOK_NAME, "") ?: "" var bookPrice = getFloat(KEY_BOOK_PRICE, 0F) var bookStr = getString(KEY_BOOK_TYPE, Type.MATH.name) var bookType: Type = Type.valueOf(bookStr ?: Type.MATH.name) return BookBean(bookName, bookPrice, bookType) } }
SharedPreferences缺点
- SP第一次加载数据时需要全量加载,当数据量大时可能会阻塞UI线程造成卡顿
- SP读写文件不是类型安全的,且没有发出错误信号的机制,缺少事务性API
- commit() / apply()操作可能会造成ANR问题:
commit()是同步提交,会在UI主线程中直接执行IO操作,当写入操作耗时比较长时就会导致UI线程被阻塞,进而产生ANR;apply()虽然是异步提交,但异步写入磁盘时,如果执行了Activity / Service中的onStop()方法,那么一样会同步等待SP写入完毕,等待时间过长时也会引起ANR问题。针对apply()我们展开来看一下:
SharedPreferencesImpl#EditorImpl.java中最终执行了apply()函数:
@Override public void apply() { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run() { try { //采用final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } ... } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); } }; //异步执行磁盘写入操作 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr); }
创建一个awaitCommit的Runnable任务并将其加入到QueuedWork中,该任务内部直接调用了CountDownLatch.await()方法,即直接在UI线程执行等待操作,那么我们看QueuedWork中何时执行这个任务。
QueuedWork.java:
public class QueuedWork { private static final LinkedList<Runnable> sFinishers = new LinkedList<>(); public static void waitToFinish() { ... Handler handler = getHandler(); try { //8.0之后优化,会主动尝试执行写磁盘任务 processPendingWork(); } finally { StrictMode.setThreadPolicy(oldPolicy); } try { while (true) { Runnable finisher; synchronized (sLock) { //从队列中取出任务 finisher = sFinishers.poll(); } //如果任务为空,则跳出循环,UI线程可以继续往下执行 if (finisher == null) { break; } //任务不为空,执行CountDownLatch.await(),即UI线程会阻塞等待 finisher.run(); } } finally { sCanDelay = true; } } }
waitToFinish()方法会尝试从Runnable任务队列中取任务,如果有的话直接取出并执行,我们看看哪里调用了waitToFinish():
ActivityThread.java
@Override public void handleStopActivity(IBinder token, int configChanges, PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) { // Make sure any pending writes are now committed. if (!r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } } private void handleStopService(IBinder token) { QueuedWork.waitToFinish(); }
可以看到在ActivityThread中handleStopActivity、handleStopService方法中都会调用waitToFinish()方法,即在Activity的onStop()中、Service的onStop()中都会先同步等待写入任务完成才会继续执行。
所以apply()虽然是异步写入磁盘,但是如果此时执行到Activity/Service的onStop(),依然可能会阻塞UI线程导致ANR。
DataStore
Jetpack DataStore
是一种改进的数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。
DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。并且可以对SP数据进行迁移,旨在取代SP。如果正在使用SharedPreferences 存储数据,请考虑迁移到 DataStore。
Jetpack DataStore 有两种实现方式:
- Preferences DataStore:以键值对的形式存储在本地类似 SharedPreferences 。
- Proto DataStore:存储类的对象(typed objects ),通过 protocol buffers 将对象序列化存储在本地。
Preferences DataStore使用
1.添加依赖项:
implementation 'androidx.datastore:datastore-preferences:1.0.0'
2.构建Preferences DataStore:
/** * TODO:创建 Preferences DataStore * 参数1:name:创建Preferences DataStore文件名称。 * 会在/data/data/项目报名/files/下创建名为pf_dataastore的文件 * 参数2:corruptionHandler:如果DataStore在试图读取数据时,数据无法反序列化,会抛出androidx.datastore.core.CorruptionException, * 此时会执行corruptionHandler。 * 参数3:produceMigrations:SP产生迁移到Preferences DataStore。ApplicationContext作为参数传递给这些回调,迁移在对数据进行任何访问之前运行。 * 参数4:scope:协成的作用域,默认IO操作在Dispatchers.IO线程执行。 */ val Context.dataStorePf: DataStore<Preferences> by preferencesDataStore( //文件名称 name = "preferences_dataStore")
当我们构建后,会在/data/data/<package name>/files/
下创建名为preferences_dataStore
的文件如下:
构建Preferences DataStore
//常量 const val BOOK_PREFERENCES_NAME = "book_preferences" const val KEY_BOOK_NAME = "key_book_name" const val KEY_BOOK_PRICE = "key_book_price" const val KEY_BOOK_TYPE = "key_book_type" /** * TODO:创建 Preferences DataStore * 参数1:name:创建Preferences DataStore文件名称。 * 会在/data/data/项目报名/files/下创建名为pf_dataastore的文件 * 参数2:corruptionHandler:如果DataStore在试图读取数据时,数据无法反序列化,会抛出androidx.datastore.core.CorruptionException, * 此时会执行corruptionHandler。 * 参数3:produceMigrations:SP产生迁移到Preferences DataStore。ApplicationContext作为参数传递给这些回调,迁移在对数据进行任何访问之前运行。 * 参数4:scope:协成的作用域,默认IO操作在Dispatchers.IO线程执行。 */ val Context.dataStorePf: DataStore<Preferences> by preferencesDataStore( name = "preferences_dataStore")
存储的实体类
data class BookBean( var name: String = "", var price: Float = 0f, var type: Type = Type.ENGLISH) { } enum class Type{ MATH, //数学 CHINESE, //语文 ENGLISH //英语 }
数据存储/获取
Activity中
//-------------------- Preferences DataStore ------------------------- /** * TODO:Preferences DataStore 保存数据 */ fun savePD(view: View) { val book = BookBean("张三", 25f, Type.CHINESE) viewModel.saveBookPD(book) } /** * TODO:Preferences DataStore 获取数据 */ fun getPD(view: View) { lifecycleScope.launch { viewModel.bookPfFlow.collect { tv_pd_data.text = it.toString() } } }
ViewModel中
//-------------------- Preferences DataStore ------------------------- /** * TODO:Preferences DataStore 保存数据 必须在协程中进行 */ fun saveBookPD(bookBean: BookBean) { viewModelScope.launch { dataStoreRepo.saveBookPD(bookBean) } } /** * TODO:Preferences DataStore 获取数据 */ val bookPfFlow = dataStoreRepo.bookPDFlow
Repository类中
//-------------------- Preferences DataStore ------------------------- /** * Preferences DataStore 存数据 */ suspend fun saveBookPD(book: BookBean) { context.dataStorePf.edit { preferences -> preferences[PreferenceKeys.P_KEY_BOOK_NAME] = book.name preferences[PreferenceKeys.P_KEY_BOOK_PRICE] = book.price preferences[PreferenceKeys.P_KEY_BOOK_TYPE] = book.type.name } } /** * Preferences DataStore 获取数据 */ val bookPDFlow: Flow<BookBean> = context.dataStorePf.data .map { preferences -> // No type safety. val name = preferences[PreferenceKeys.P_KEY_BOOK_NAME] ?: "" val bookPrice = preferences[PreferenceKeys.P_KEY_BOOK_PRICE] ?: 0f val bookType = Type.valueOf(preferences[PreferenceKeys.P_KEY_BOOK_TYPE] ?: Type.MATH.name) return@map BookBean(name, bookPrice, bookType) }
SP迁移至Preferences DataStore如果想将项目的SP进行迁移,只需要在Preferences DataStore在构建时配置参数3,如下:
//SharedPreference文件名 const val BOOK_PREFERENCES_NAME = "book_preferences" val Context.dataStorePf: DataStore<Preferences> by preferencesDataStore( name = "preferences_dataStore", //将SP迁移到Preference DataStore中 produceMigrations = { context -> listOf(SharedPreferencesMigration(context, BOOK_PREFERENCES_NAME)) })
这样构建完成时,SP中的内容也会迁移到Preferences DataStore中了,注意迁移是一次性的,即执行迁移后,SP文件会被删除.
MMKV
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。
原理
- 内存准备通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
- 数据组织数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
- 写入优化考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
- 空间增长使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
使用
1.添加依赖:
implementation 'com.tencent:mmkv:1.2.13'
2.Application的onCreate方法中初始化
class App:Application() { override fun onCreate() { super.onCreate() val rootDir = MMKV.initialize(this) Log.e("TAG","mmkv root: $rootDir") } }
3.数据存储/获取:
MMKV kv = MMKV.defaultMMKV(); kv.encode("bool", true); boolean bValue = kv.decodeBool("bool"); kv.encode("int", Integer.MIN_VALUE); int iValue = kv.decodeInt("int"); kv.encode("string", "Hello from mmkv"); String str = kv.decodeString("string");
以上就是Android轻量级存储SharedPreferences MMKV Jetpack DataStore方案的详细内容,更多关于Android轻量级存储的资料请关注脚本之家其它相关文章!