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

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缺点:
  1. SP第一次加载数据时需要全量加载,当数据量大时可能会阻塞UI线程造成卡顿
  2. SP读写文件不是类型安全的,且没有发出错误信号的机制,缺少事务性API
  3. 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容