找回密码
 立即注册
查看: 268|回复: 0

Android 轻量级存储方案(SharedPreferences、MMKV、Jetpack DataStore)

[复制链接]
发表于 2022-6-24 18:20 | 显示全部楼层 |阅读模式
1.SharePreferences
SharedPreferences:一个轻量级的存储类,特别适合用于保存应用配置参数。(是用xml文件存放数据,文件存放在/data/data/<package name>/shared_prefs目录下)

image.png

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)            }        }4.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。
2.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的文件如下:


image.png

Preferences DataStore使用:


1.构建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")
2.存储的实体类:
data class BookBean( var name: String = "",                 var price: Float = 0f,                 var type: Type = Type.ENGLISH) {}enum class Type{    MATH,       //数学    CHINESE,    //语文    ENGLISH     //英语}
3.数据存储/获取:
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文件会被删除.
3.MMKV


MMKV 是基于 mmap 内存映射的 key-value 件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。
MMKV 原理:


  • 内存准备
    通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
  • 数据组织
    数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
  • 写入优化
    考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
  • 空间增长
    使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
MMKV使用:


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");
github地址:https://github.com/HuiZaierr/Android_Store

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-26 07:41 , Processed in 0.065200 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表