Android輕量級存儲SharedPreferences?MMKV?Jetpack?DataStore方案
SharePreferences使用
SharedPreferences:一個(gè)輕量級的存儲類,特別適合用于保存應(yīng)用配置參數(shù)。(是用xml文件存放數(shù)據(jù),文件存放在/data/data/<package name>/shared_prefs目錄下)
SharedPreferences用法:
1.保存數(shù)據(jù)
保存數(shù)據(jù)一般分為以下步驟:
使用Activity類的getSharedPreferences方法獲得SharedPreferences對象;
使用SharedPreferences接口的edit獲得SharedPreferences.Editor對象;
通過SharedPreferences.Editor接口的putXXX方法保存key-value對;
通過過SharedPreferences.Editor接口的commit方法保存key-value對。
2.讀取數(shù)據(jù)
使用Activity類的getSharedPreferences方法獲得SharedPreferences對象;
通過SharedPreferences對象的getXXX方法獲取數(shù)據(jù);
3.示例
//-------------------- SharePreferences ------------------------- //獲取SharePreferences private val sp = context.applicationContext.getSharedPreferences(BOOK_PREFERENCES_NAME, MODE_PRIVATE) /** * SharePreferences 存數(shù)據(jù) */ fun saveBookSP(book: BookBean) { //commit默認(rèn)為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 獲取數(shù)據(jù) */ 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缺點(diǎn)
- SP第一次加載數(shù)據(jù)時(shí)需要全量加載,當(dāng)數(shù)據(jù)量大時(shí)可能會阻塞UI線程造成卡頓
- SP讀寫文件不是類型安全的,且沒有發(fā)出錯(cuò)誤信號的機(jī)制,缺少事務(wù)性API
- commit() / apply()操作可能會造成ANR問題:
commit()是同步提交,會在UI主線程中直接執(zhí)行IO操作,當(dāng)寫入操作耗時(shí)比較長時(shí)就會導(dǎo)致UI線程被阻塞,進(jìn)而產(chǎn)生ANR;apply()雖然是異步提交,但異步寫入磁盤時(shí),如果執(zhí)行了Activity / Service中的onStop()方法,那么一樣會同步等待SP寫入完畢,等待時(shí)間過長時(shí)也會引起ANR問題。針對apply()我們展開來看一下:
SharedPreferencesImpl#EditorImpl.java中最終執(zhí)行了apply()函數(shù):
@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); } }; //異步執(zhí)行磁盤寫入操作 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr); }
創(chuàng)建一個(gè)awaitCommit的Runnable任務(wù)并將其加入到QueuedWork中,該任務(wù)內(nèi)部直接調(diào)用了CountDownLatch.await()方法,即直接在UI線程執(zhí)行等待操作,那么我們看QueuedWork中何時(shí)執(zhí)行這個(gè)任務(wù)。
QueuedWork.java:
public class QueuedWork { private static final LinkedList<Runnable> sFinishers = new LinkedList<>(); public static void waitToFinish() { ... Handler handler = getHandler(); try { //8.0之后優(yōu)化,會主動嘗試執(zhí)行寫磁盤任務(wù) processPendingWork(); } finally { StrictMode.setThreadPolicy(oldPolicy); } try { while (true) { Runnable finisher; synchronized (sLock) { //從隊(duì)列中取出任務(wù) finisher = sFinishers.poll(); } //如果任務(wù)為空,則跳出循環(huán),UI線程可以繼續(xù)往下執(zhí)行 if (finisher == null) { break; } //任務(wù)不為空,執(zhí)行CountDownLatch.await(),即UI線程會阻塞等待 finisher.run(); } } finally { sCanDelay = true; } } }
waitToFinish()方法會嘗試從Runnable任務(wù)隊(duì)列中取任務(wù),如果有的話直接取出并執(zhí)行,我們看看哪里調(diào)用了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方法中都會調(diào)用waitToFinish()方法,即在Activity的onStop()中、Service的onStop()中都會先同步等待寫入任務(wù)完成才會繼續(xù)執(zhí)行。
所以apply()雖然是異步寫入磁盤,但是如果此時(shí)執(zhí)行到Activity/Service的onStop(),依然可能會阻塞UI線程導(dǎo)致ANR。
DataStore
Jetpack DataStore
是一種改進(jìn)的數(shù)據(jù)存儲解決方案,允許您使用協(xié)議緩沖區(qū)存儲鍵值對或類型化對象。
DataStore 使用 Kotlin 協(xié)程和 Flow 以異步、一致的事務(wù)方式存儲數(shù)據(jù)。并且可以對SP數(shù)據(jù)進(jìn)行遷移,旨在取代SP。如果正在使用SharedPreferences 存儲數(shù)據(jù),請考慮遷移到 DataStore。
Jetpack DataStore 有兩種實(shí)現(xiàn)方式:
- Preferences DataStore:以鍵值對的形式存儲在本地類似 SharedPreferences 。
- Proto DataStore:存儲類的對象(typed objects ),通過 protocol buffers 將對象序列化存儲在本地。
Preferences DataStore使用
1.添加依賴項(xiàng):
implementation 'androidx.datastore:datastore-preferences:1.0.0'
2.構(gòu)建Preferences DataStore:
/** * TODO:創(chuàng)建 Preferences DataStore * 參數(shù)1:name:創(chuàng)建Preferences DataStore文件名稱。 * 會在/data/data/項(xiàng)目報(bào)名/files/下創(chuàng)建名為pf_dataastore的文件 * 參數(shù)2:corruptionHandler:如果DataStore在試圖讀取數(shù)據(jù)時(shí),數(shù)據(jù)無法反序列化,會拋出androidx.datastore.core.CorruptionException, * 此時(shí)會執(zhí)行corruptionHandler。 * 參數(shù)3:produceMigrations:SP產(chǎn)生遷移到Preferences DataStore。ApplicationContext作為參數(shù)傳遞給這些回調(diào),遷移在對數(shù)據(jù)進(jìn)行任何訪問之前運(yùn)行。 * 參數(shù)4:scope:協(xié)成的作用域,默認(rèn)IO操作在Dispatchers.IO線程執(zhí)行。 */ val Context.dataStorePf: DataStore<Preferences> by preferencesDataStore( //文件名稱 name = "preferences_dataStore")
當(dāng)我們構(gòu)建后,會在/data/data/<package name>/files/
下創(chuàng)建名為preferences_dataStore
的文件如下:
構(gòu)建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:創(chuàng)建 Preferences DataStore * 參數(shù)1:name:創(chuàng)建Preferences DataStore文件名稱。 * 會在/data/data/項(xiàng)目報(bào)名/files/下創(chuàng)建名為pf_dataastore的文件 * 參數(shù)2:corruptionHandler:如果DataStore在試圖讀取數(shù)據(jù)時(shí),數(shù)據(jù)無法反序列化,會拋出androidx.datastore.core.CorruptionException, * 此時(shí)會執(zhí)行corruptionHandler。 * 參數(shù)3:produceMigrations:SP產(chǎn)生遷移到Preferences DataStore。ApplicationContext作為參數(shù)傳遞給這些回調(diào),遷移在對數(shù)據(jù)進(jìn)行任何訪問之前運(yùn)行。 * 參數(shù)4:scope:協(xié)成的作用域,默認(rèn)IO操作在Dispatchers.IO線程執(zhí)行。 */ val Context.dataStorePf: DataStore<Preferences> by preferencesDataStore( name = "preferences_dataStore")
存儲的實(shí)體類
data class BookBean( var name: String = "", var price: Float = 0f, var type: Type = Type.ENGLISH) { } enum class Type{ MATH, //數(shù)學(xué) CHINESE, //語文 ENGLISH //英語 }
數(shù)據(jù)存儲/獲取
Activity中
//-------------------- Preferences DataStore ------------------------- /** * TODO:Preferences DataStore 保存數(shù)據(jù) */ fun savePD(view: View) { val book = BookBean("張三", 25f, Type.CHINESE) viewModel.saveBookPD(book) } /** * TODO:Preferences DataStore 獲取數(shù)據(jù) */ fun getPD(view: View) { lifecycleScope.launch { viewModel.bookPfFlow.collect { tv_pd_data.text = it.toString() } } }
ViewModel中
//-------------------- Preferences DataStore ------------------------- /** * TODO:Preferences DataStore 保存數(shù)據(jù) 必須在協(xié)程中進(jìn)行 */ fun saveBookPD(bookBean: BookBean) { viewModelScope.launch { dataStoreRepo.saveBookPD(bookBean) } } /** * TODO:Preferences DataStore 獲取數(shù)據(jù) */ val bookPfFlow = dataStoreRepo.bookPDFlow
Repository類中
//-------------------- Preferences DataStore ------------------------- /** * Preferences DataStore 存數(shù)據(jù) */ 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 獲取數(shù)據(jù) */ 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如果想將項(xiàng)目的SP進(jìn)行遷移,只需要在Preferences DataStore在構(gòu)建時(shí)配置參數(shù)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)) })
這樣構(gòu)建完成時(shí),SP中的內(nèi)容也會遷移到Preferences DataStore中了,注意遷移是一次性的,即執(zhí)行遷移后,SP文件會被刪除.
MMKV
MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實(shí)現(xiàn),性能高,穩(wěn)定性強(qiáng)。
原理
- 內(nèi)存準(zhǔn)備通過 mmap 內(nèi)存映射文件,提供一段可供隨時(shí)寫入的內(nèi)存塊,App 只管往里面寫數(shù)據(jù),由操作系統(tǒng)負(fù)責(zé)將內(nèi)存回寫到文件,不必?fù)?dān)心 crash 導(dǎo)致數(shù)據(jù)丟失。
- 數(shù)據(jù)組織數(shù)據(jù)序列化方面我們選用 protobuf 協(xié)議,pb 在性能和空間占用上都有不錯(cuò)的表現(xiàn)。
- 寫入優(yōu)化考慮到主要使用場景是頻繁地進(jìn)行寫入更新,我們需要有增量更新的能力。我們考慮將增量 kv 對象序列化后,append 到內(nèi)存末尾。
- 空間增長使用 append 實(shí)現(xiàn)增量更新帶來了一個(gè)新的問題,就是不斷 append 的話,文件大小會增長得不可控。我們需要在性能和空間上做個(gè)折中。
使用
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.數(shù)據(jù)存儲/獲?。?/strong>
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方案的詳細(xì)內(nèi)容,更多關(guān)于Android輕量級存儲的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android獲取實(shí)時(shí)連接熱點(diǎn)的設(shè)備IP
這篇文章主要介紹了Android獲取實(shí)時(shí)連接熱點(diǎn)的設(shè)備IP 的相關(guān)資料,文中給大家補(bǔ)充介紹了安卓獲取接入的Wifi熱點(diǎn)設(shè)備的Ip地址的代碼,需要的朋友可以參考下2018-01-01Jetpack Compose自定義動畫與Animatable詳解
在今年的Google/IO大會上,亮相了一個(gè)全新的 Android 原生 UI 開發(fā)框架-Jetpack Compose, 與蘋果的SwiftIUI一樣,Jetpack Compose是一個(gè)聲明式的UI框架,這篇文章主要介紹了Jetpack Compose自定義動畫與Animatable2022-10-10Android自定義View實(shí)現(xiàn)游戲搖桿鍵盤的方法示例
Android進(jìn)階過程中有一個(gè)繞不開的話題——自定義View。最近在做項(xiàng)目中又遇到了,所以下面這篇文章主要給大家介紹了利用Android自定義View實(shí)現(xiàn)游戲搖桿鍵盤的相關(guān)資料,操作方式類似王者榮耀的搖桿操作,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面來一起看看吧。2017-07-07Flutter開發(fā)技巧RadialGradient中radius計(jì)算詳解
這篇文章主要為大家介紹了Flutter小技巧RadialGradient?中?radius?的計(jì)算詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01將cantk runtime嵌入到現(xiàn)有的APP中的方法
今天小編就為大家分享一篇關(guān)于將cantk runtime嵌入到現(xiàn)有的APP中的方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2018-12-12Android?Jetpack組件ViewModel基本用法詳解
這篇文章主要為大家介紹了Android?Jetpack組件ViewModel基本用法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01