Google 新增加了一个新 Jetpack 的成员 DataStore,主要用来替换 SharedPreferences, DataStore 应该是开发者期待已久的库,DataStore 是基于 Flow 实现的,一种新的数据存储方案,它提供了两种实现方式:

  • Proto DataStore:存储类的对象(typed objects ),通过 protocol buffers 将对象序列化存储在本地,protocol buffers 现在已经应用的非常广泛,无论是微信还是阿里等等大厂都在使用,我们在部分业务场景中也用到了 protocol buffers,会在后续的文章详细分析

  • Preferences DataStore:以键值对的形式存储在本地和 SharedPreferences 类似,但是 DataStore 是基于 Flow 实现的,不会阻塞主线程,并且保证类型安全

Jetpack DataStore 将会分为至少 2 篇文章来分析,今天这篇文章主要来介绍 Jetpack DataStore 其中一种实现方式 Preferences DataStore,文章中的示例代码,已经上传到 GitHub 欢迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple

GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

这篇文章会涉及到 Koltin flow 相关内容,如果不了解可以先去看另外一篇文章 Kotlin Flow 是什么?Channel 是什么?

通过这篇文章你将学习到以下内容:

  • 那些年我们所经历的 SharedPreferences 坑?
  • 为什么需要 DataStore?它为我们解决了什么问题?
  • 如何在项目中使用 DataStore?
  • 如何迁移 SharedPreferences 到 DataStore?
  • MMKV、DataStore、SharedPreferences 的不同之处?

一个新库的出现必定为我们解决了一些问题,那么 Jetpack DataStore 为我们解决什么问题呢,在分析之前,我们需要先来了解 SharedPreferences 都有那些坑。

那些年我们所经历的 SharedPreferences 坑

SharedPreference 是一个轻量级的数据存储方式,使用起来也非常方便,以键值对的形式存储在本地,初始化 SharedPreference 的时候,会将整个文件内容加载内存中,因此会带来以下问题:

  • 通过 getXXX() 方法获取数据,可能会导致主线程阻塞
  • SharedPreference 不能保证类型安全
  • SharedPreference 加载的数据会一直留在内存中,浪费内存
  • apply() 方法虽然是异步的,可能会发生 ANR,在 8.0 之前和 8.0 之后实现各不相同
  • apply() 方法无法获取到操作成功或者失败的结果

接下来我们逐个来分析一下 SharedPreferences 带来的这些问题,在文章中 SharedPreference 简称 SP。

getXXX() 方法可能会导致主线程阻塞

所有 getXXX() 方法都是同步的,在主线程调用 get 方法,必须等待 SP 加载完毕,会导致主线程阻塞,下面的代码,我相信小伙伴们并不陌生。

val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 异步加载 SP 文件内容
sp.getString("jetpack", ""); // 等待 SP 加载完毕

调用 getSharedPreferences() 方法,最终会调用 SharedPreferencesImpl#startLoadFromDisk() 方法开启一个线程异步读取数据。
frameworks/base/core/java/android/app/SharedPreferencesImpl.java

private final Object mLock = new Object();
private boolean mLoaded = false;
private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();
}

正如你所看到的,开启一个线程异步读取数据,当我们正在读取一个比较大的数据,还没读取完,接着调用 getXXX() 方法。

public String getString(String key, @Nullable String defValue) {synchronized (mLock) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;}
}private void awaitLoadedLocked() {......while (!mLoaded) {try {mLock.wait();} catch (InterruptedException unused) {}}......
}

在同步方法内调用了 wait() 方法,会一直等待 getSharedPreferences() 方法开启的线程读取完数据才能继续往下执行,如果读取几 KB 的数据还好,假设读取一个大的文件,势必会造成主线程阻塞。

SP 不能保证类型安全

调用 getXXX() 方法的时候,可能会出现 ClassCastException 异常,因为使用相同的 key 进行操作的时候,putXXX 方法可以使用不同类型的数据覆盖掉相同的 key。

val key = "jetpack"
val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 异步加载 SP 文件内容sp.edit { putInt(key, 0) } // 使用 Int 类型的数据覆盖相同的 key
sp.getString(key, ""); // 使用相同的 key 读取 Sting 类型的数据

使用 Int 类型的数据覆盖掉相同的 key,然后使用相同的 key 读取 Sting 类型的数据,编译正常,但是运行会出现以下异常。

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

SP 加载的数据会一直留在内存中

通过 getSharedPreferences() 方法加载的数据,最后会将数据存储在静态的成员变量中。

// 调用 getSharedPreferences 方法,最后会调用 getSharedPreferencesCacheLocked 方法
public SharedPreferences getSharedPreferences(File file, int mode) {......final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();return sp;
}// 通过静态的 ArrayMap 缓存 SP 加载的数据
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;// 将数据保存在 sSharedPrefsCache 中
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {......ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);if (packagePrefs == null) {packagePrefs = new ArrayMap<>();sSharedPrefsCache.put(packageName, packagePrefs);}return packagePrefs;
}

通过静态的 ArrayMap 缓存每一个 SP 文件,而每个 SP 文件内容通过 Map 缓存键值对数据,这样数据会一直留在内存中,浪费内存。

apply() 方法是异步的,可能会发生 ANR

apply() 方法是异步的,为什么还会造成 ANR 呢?曾今的字节跳动就出现过这个问题,具体详情可以点击这里前去查看 剖析 SharedPreference apply 引起的 ANR 问题 而且 Google 也明确指出了 apply() 的问题。

简单总结一下:apply() 方法是异步的,本身是不会有任何问题,但是当生命周期处于 handleStopService()handlePauseActivity()handleStopActivity() 的时候会一直等待 apply() 方法将数据保存成功,否则会一直等待,从而阻塞主线程造成 ANR,一起来分析一下为什么异步方法还会阻塞主线程,先来看看 apply() 方法的实现。
frameworks/base/core/java/android/app/SharedPreferencesImpl.java

public void apply() {final long startTime = System.currentTimeMillis();final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {mcr.writtenToDiskLatch.await(); // 等待......}};// 将 awaitCommit 添加到队列 QueuedWork 中QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};// 8.0 之前加入到一个单线程的线程池中执行// 8.0 之后加入 HandlerThread 中执行写入任务SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
  • 将一个 awaitCommit 的 Runnable 任务,添加到队列 QueuedWork 中,在 awaitCommit 中会调用 await() 方法等待,在 handleStopServicehandleStopActivity 等等生命周期会以这个作为判断条件,等待任务执行完毕
  • 将一个 postWriteRunnable 的 Runnable 写任务,通过 enqueueDiskWrite 方法,将写入任务加入到队列中,而写入任务在一个线程中执行

注意:在 8.0 之前和 8.0 之后 enqueueDiskWrite() 方法实现逻辑各不相同

在 8.0 之前调用 enqueueDiskWrite() 方法,将写入任务加入到 单个线程的线程池 中执行,如果 apply() 多次的话,任务将会依次执行,效率很低,android-7.0.0_r34 源码如下所示。

// android-7.0.0_r34: frameworks/base/core/java/android/app/SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {......QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}// android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java
public static ExecutorService singleThreadExecutor() {synchronized (QueuedWork.class) {if (sSingleThreadExecutor == null) {sSingleThreadExecutor = Executors.newSingleThreadExecutor();}return sSingleThreadExecutor;}
}

通过 Executors.newSingleThreadExecutor() 方法创建了一个 单个线程的线程池,因此任务是串行的,通过 apply() 方法创建的任务,都会添加到这个线程池内。

在 8.0 之后将写入任务加入到 LinkedList 链表中,在 HandlerThread 中执行写入任务,android-10.0.0_r14 源码如下所示。

// android-10.0.0_r14: frameworks/base/core/java/android/app/SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {......QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}// android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.javaprivate static final LinkedList<Runnable> sWork = new LinkedList<>();public static void queue(Runnable work, boolean shouldDelay) {Handler handler = getHandler(); // 获取 handlerThread.getLooper() 生成 Handler 对象synchronized (sLock) {sWork.add(work); // 将写入任务加入到 LinkedList 链表中if (shouldDelay && sCanDelay) {handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);} else {handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);}}
}

在 8.0 之后通过调用 handlerThread.getLooper() 方法生成 Handler,任务都会在 HandlerThread 中执行,所有通过 apply() 方法创建的任务,都会添加到 LinkedList 链表中。

当生命周期处于 handleStopService()handlePauseActivity()handleStopActivity() 的时候会调用 QueuedWork.waitToFinish() 会等待写入任务执行完毕,我们以其中 handlePauseActivity() 方法为例。

public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,int configChanges, PendingTransactionActions pendingActions, String reason) {......// 确保写任务都已经完成QueuedWork.waitToFinish();......}
}

正如你所看到的在 handlePauseActivity() 方法中,调用了 QueuedWork.waitToFinish() 方法,会等待所有的写入执行完毕,Google 在 8.0 之后对这个方法做了很大的优化,一起来看一下 8.0 之前和 8.0 之后的区别。

注意:在 8.0 之前和 8.0 之后 waitToFinish() 方法实现逻辑各不相同

在 8.0 之前 waitToFinish() 方法只做了一件事,会一直等待写入任务执行完毕,我先来看看在 android-7.0.0_r34 源码实现。
android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java

private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =new ConcurrentLinkedQueue<Runnable>();public static void waitToFinish() {Runnable toFinish;while ((toFinish = sPendingWorkFinishers.poll()) != null) {toFinish.run(); // 相当于调用 `mcr.writtenToDiskLatch.await()` 方法}
}
  • sPendingWorkFinishers 是 ConcurrentLinkedQueue 实例,apply 方法会将写入任务添加到 sPendingWorkFinishers 队列中,在 单个线程的线程池 中执行写入任务,线程的调度并不由程序来控制,也就是说当生命周期切换的时候,任务不一定处于执行状态

  • toFinish.run() 方法,相当于调用 mcr.writtenToDiskLatch.await() 方法,会一直等待

  • waitToFinish() 方法就做了一件事,会一直等待写入任务执行完毕,其它什么都不做,当有很多写入任务,会依次执行,当文件很大时,效率很低,造成 ANR 就不奇怪了,尤其像字节跳动这种大规模的 App

在 8.0 之后 waitToFinish() 方法做了很大的优化,当生命周期切换的时候,会主动触发任务的执行,而不是一直在等着,我们来看看 android-10.0.0_r14 源码实现。
android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java

private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
public static void waitToFinish() {......try {processPendingWork(); // 主动触发任务的执行} finally {StrictMode.setThreadPolicy(oldPolicy);}try {// 等待任务执行完毕while (true) {Runnable finisher;synchronized (sLock) {finisher = sFinishers.poll(); // 从 LinkedList 中取出任务}if (finisher == null) { // 当 LinkedList 中没有任务时会跳出循环break;}finisher.run(); // 相当于调用 `mcr.writtenToDiskLatch.await()`}} ......
}

waitToFinish() 方法中会主动调用 processPendingWork() 方法触发任务的执行,在 HandlerThread 中执行写入任务。

另外还做了一个很重要的优化,当调用 apply() 方法的时候,执行磁盘写入,都是全量写入,在 8.0 之前,调用 N 次 apply() 方法,就会执行 N 次磁盘写入,在 8.0 之后,apply() 方法调用了多次,只会执行最后一次写入,通过版本号来控制的。

SharedPreferences 的另外一个缺点就是 apply() 方法无法获取到操作成功或者失败的结果,而 commit() 方法是可以接收 MemoryCommitResult 里面的一个 boolean 参数作为结果,来看一下它们的方法签名。

public void apply() { ... }public boolean commit() { ... }

SP 不能用于跨进程通信

我们在创建 SP 实例的时候,需要传入一个 mode,如下所示:

val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE)

Context 内部还有一个 modeMODE_MULTI_PROCESS,我们来看一下这个 mode 做了什么

public SharedPreferences getSharedPreferences(File file, int mode) {if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {// 重新读取 SP 文件内容sp.startReloadIfChangedUnexpectedly();}return sp;
}

在这里就做了一件事,当遇到 MODE_MULTI_PROCESS 的时候,会重新读取 SP 文件内容,并不能用 SP 来做跨进程通信。

到这里关于 SharedPreferences 部分分析完了,接下来分析一下 DataStore 为我们解决什么问题?

DataStore 解决了什么问题

Preferences DataStore 主要用来替换 SharedPreferences,Preferences DataStore 解决了 SharedPreferences 带来的所有问题

Preferences DataStore 相比于 SharedPreferences 优点

  • DataStore 是基于 Flow 实现的,所以保证了在主线程的安全性
  • 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性)
  • 没有 apply()commit() 等等数据持久的方法
  • 自动完成 SharedPreferences 迁移到 DataStore,保证数据一致性,不会造成数据损坏
  • 可以监听到操作成功或者失败结果

另外 Jetpack DataStore 提供了 Proto DataStore 方式,用于存储类的对象(typed objects ),通过 protocol buffers 将对象序列化存储在本地,protocol buffers 现在已经应用的非常广泛,无论是微信还是阿里等等大厂都在使用,我们在部分场景中也使用了 protocol buffers,在后续的文章会详细的分析。

注意:

Preferences DataStore 只支持 Int , Long , Boolean , Float , String 键值对数据,适合存储简单、小型的数据,并且不支持局部更新,如果修改了其中一个值,整个文件内容将会被重新序列化,可以运行 AndroidX-Jetpack-Practice/DataStoreSimple 体验一下,如果需要局部更新,建议使用 Room。

在项目中使用 Preferences DataStore

Preferences DataStore 主要应用在 MVVM 当中的 Repository 层,在项目中使用 Preferences DataStore 非常简单,只需要 4 步。

1. 需要添加 Preferences DataStore 依赖

implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

2. 构建 DataStore

private val PREFERENCE_NAME = "DataStore"
var dataStore: DataStore<Preferences> = context.createDataStore(name = PREFERENCE_NAME

3. 从 Preferences DataStore 中读取数据

Preferences DataStore 以键值对的形式存储在本地,所以首先我们应该定义一个 Key.

val KEY_BYTE_CODE = preferencesKey<Boolean>("ByteCode")

这里和我们之前使用 SharedPreferences 的有点不一样,在 Preferences DataStore 中 Key 是一个 Preferences.Key<T> 类型,只支持 Int , Long , Boolean , Float , String,源码如下所示:

inline fun <reified T : Any> preferencesKey(name: String): Preferences.Key<T> {return when (T::class) {Int::class -> {Preferences.Key<T>(name)}String::class -> {Preferences.Key<T>(name)}Boolean::class -> {Preferences.Key<T>(name)}Float::class -> {Preferences.Key<T>(name)}Long::class -> {Preferences.Key<T>(name)}...... // 如果是其他类型就会抛出异常}
}

当我们定义好 Key 之后,就可以通过 dataStore.data 来获取数据

override fun readData(key: Preferences.Key<Boolean>): Flow<Boolean> =dataStore.data.catch {// 当读取数据遇到错误时,如果是 `IOException` 异常,发送一个 emptyPreferences 来重新使用// 但是如果是其他的异常,最好将它抛出去,不要隐藏问题if (it is IOException) {it.printStackTrace()emit(emptyPreferences())} else {throw it}}.map { preferences ->preferences[key] ?: false}
  • Preferences DataStore 是基于 Flow 实现的,所以通过 dataStore.data 会返回一个 Flow<T>,每当数据变化的时候都会重新发出
  • catch 用来捕获异常,当读取数据出现异常时会抛出一个异常,如果是 IOException 异常,会发送一个 emptyPreferences() 来重新使用,如果是其他异常,最好将它抛出去

4. 向 Preferences DataStore 中写入数据

在 Preferences DataStore 中是通过 DataStore.edit() 写入数据的,DataStore.edit() 是一个 suspend 函数,所以只能在协程体内使用,每当遇到 suspend 函数以挂起的方式运行,并不会阻塞主线程。

以挂起的方式运行,不会阻塞主线程 :也就是协程作用域被挂起, 当前线程中协程作用域之外的代码不会阻塞。

首先我们需要创建一个 suspend 函数,然后调用 DataStore.edit() 写入数据即可。

override suspend fun saveData(key: Preferences.Key<Boolean>) {dataStore.edit { mutablePreferences ->val value = mutablePreferences[key] ?: falsemutablePreferences[key] = !value}
}

到这里关于 Preferences DataStore 读取数据和写入数据就已经分析完了,接下来分析一下如何迁移 SharedPreferences 到 DataStore

迁移 SharedPreferences 到 DataStore

迁移 SharedPreferences 到 DataStore 只需要 2 步。

  • 在构建 DataStore 的时候,需要传入一个 SharedPreferencesMigration
dataStore = context.createDataStore(name = PREFERENCE_NAME,migrations = listOf(SharedPreferencesMigration(context,SharedPreferencesRepository.PREFERENCE_NAME))
)
  • 当 DataStore 对象构建完了之后,需要执行一次读取或者写入操作,即可完成 SharedPreferences 迁移到 DataStore,当迁移成功之后,会自动删除 SharedPreferences 使用的文件

注意: 只从 SharedPreferences 迁移一次,因此一旦迁移成功之后,应该停止使用 SharedPreferences。

相比于 MMKV 有什么不同之处

最后用一张表格来对比一下 MMKV、DataStore、SharedPreferences 的不同之处,如果发现错误,或者有其他不同之处,期待你来一起完善。

另外在附上一张 Google 分析的 SharedPreferences 和 DataStore 的区别

全文到这里就结束了,这篇文章主要分析了 SharedPreferences 和 DataStore 的优缺点,以及为什么需要引入 DataStore 和如何使用 DataStore,为了节省篇幅源码分析部分会在后续的文章中分析。

关于 SharedPreferences 和 DataStore 相关的代码,已经上传到了 GitHub 欢迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple ,可以运行一下示例项目,体验一下 SharedPreferences 和 DataStore 效果。

  • GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

参考文献

  • Preferences DataStore codelab
  • Now in Android #25
  • Prefer Storing Data with Jetpack DataStore
  • 剖析 SharedPreference 引起的 ANR 问题
  • SharedPreferences 问题分析和解决

结语

致力于一系列 Android 系统源码、逆向分析、算法、译文、Kotlin、Jetpack 源码相关的文章,如果这篇文章对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。

在国庆期间我梳理了 LeetCode / 剑指 offer 及国内外大厂面试题解,截止到目前为止我已经在 LeetCode 上 AC 了 124+ 题,每题都会用 Java 和 kotlin 去实现,并且每题都有多种解法、解题思路、时间复杂度、空间复杂度分析,题库逐渐完善中,欢迎前去查看。

  • 剑指 offer 及国内外大厂面试题解:在线阅读
  • LeetCode 系列题解:在线阅读


最后推荐我一直在更新维护的项目和网站:

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice

  • LeetCode / 剑指 Offer / 国内外大厂面试题,涵盖: 多线程、数组、栈、队列、字符串、链表、树,查找算法、搜索算法、位运算、排序等等,每道题目都会用 Java 和 kotlin 去实现,仓库持续更新,欢迎前去查看 Leetcode-Solutions-with-Java-And-Kotlin,剑指 offer 及国内外大厂面试题解:在线阅读,LeetCode 系列题解:在线阅读

  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis

  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation

  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看 为互联网人而设计导航网站

[Google] 再见 SharedPreferences 拥抱 Jetpack DataStore相关推荐

  1. 安卓 sharedpreferences可以被其它activity读取_Google|再见 SharedPreferences 拥抱 Jetpack DataStore...

    Google 新增加了一个新 Jetpack 的成员 DataStore,主要用来替换 SharedPreferences, DataStore 应该是开发者期待已久的库,DataStore 是基于 ...

  2. 使用 Jetpack DataStore 进行数据存储

    Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象.DataStore 使用 Kotlin 协程和流程以异步.一致的事务方式存储数据. 如果您当前使 ...

  3. JetPack—DataStore核心原理与使用

    简介 首先,DataStore是Jetpack一部分,是一种数据存储解决方案. 其次,DataStore使用协程及flow以异步.一致的方式实现数据的存储. 最后是DataStore的实现,分为Pre ...

  4. Android知识大纲

    Android知识大纲 Java垃圾回收机制 Java内存是如何划分的,Java语言为什么要使用垃圾回收机制? 垃圾判定 1. 标记引用算法 2. 根搜索法 虚拟机栈中的引用对象 方法区中的常量引用对 ...

  5. 大佬教你极简方法来处理Android SharedPreferences设计与实现

    起源 就在前几日,有幸拜读到 HiDhl 的文章,继腾讯开源类似功能的MMKV之后,Google官方维护的 Jetpack DataStore 组件横空出世--这是否意味着无论是腾讯三方还是Googl ...

  6. sharedpreferences使用方法_Google 推荐在 MVVM 架构中使用 Kotlin Flow

    前言 在之前分享过一篇 Jetpack 综合实战应用 Jetpack 实战:神奇宝贝 ,这个项目主要包了以下功能: 自定义 RemoteMediator 实现 network + db 的混合使用 ( ...

  7. 官方也无力回天?“SharedPreferences 存在什么问题?”

    作者:却把清梅嗅 链接:https://juejin.im/post/6884505736836022280 起源 就在前几日,有幸拜读到 HiDhl 的文章  https://juejin.im/p ...

  8. 反思|官方也无力回天?Android SharedPreferences的设计与实现

    反思 系列博客是我的一种新学习方式的尝试,该系列起源和目录请参考 这里 . 起源 就在前几日,有幸拜读到 HiDhl 的文章,继腾讯开源类似功能的MMKV之后,Google官方维护的 Jetpack ...

  9. 【Android Jetpack】DataStore

    目录 1. 前言 2. 分类 2.1 Preferences DataStore 和SharedPreferences的区别 3. 实践 3.1 Preferences DataStore 3.1.1 ...

  10. Android Jetpack组件DataStore之Proto与Preferences存储详解与使用

    一.介绍 Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象.DataStore 使用 Kotlin 协程和 Flow 以异步.一致的事务方式存储数 ...

最新文章

  1. 肝!Python 网络编程
  2. python读取json格式的超参数
  3. sql学习之笔记(时间)季度的第一天
  4. 借贷记账法下的账户对应关系_会计实操借贷记账法记账规则——会计干货来了快记啊!...
  5. java建立新文件保存数据_关于java中创建文件,并且写入内容
  6. canvas实现抽奖插件—大转盘和九宫格
  7. android 解码 gif 时间,Android 平台实现Gif 图像解码并播放代码及组件
  8. oracle工程师 的职业,数据库工程师的职业规划
  9. UI——day3.IOS设计规范
  10. This License XXX has been cancelled
  11. 网络存储NAS网络存储器术语解释
  12. 三次样条(Cubic Spline)的C++实现以及可视化
  13. CSS---各种分割线
  14. 像这2个案例的项目进度延误,如何破?
  15. Excel填充空白的单元格
  16. java 性能优化小细节
  17. 计算机网络里面ap是什么,无线AP是什么
  18. 我们为什么要将图片转换成PDF?
  19. Sparkthrift-sql执行报错-File does not exist: hdfs://xxx/t_bd_materialgroup/xxx.parquet
  20. win10很多软件显示模糊_显示字体小到有些模糊?高分屏别忘了这些设置

热门文章

  1. 高并发大流量,大麦抢票的技术涅槃之路
  2. 删除mac开机启动项
  3. 机器人开发--AGV控制系统
  4. 人工智能的优点是什么?AI有哪些优势?
  5. mac 输入法/键盘 锁定
  6. 国五条催生末班车效应 郑州二手房交易量激增
  7. 怎样固定计算机桌面背景,Win7桌面背景老是被修改如何将其锁定不让他人随意修改...
  8. 对象图(Object Diagram)
  9. 机器学习-对线性回归、逻辑回归、各种回归的概念学习
  10. 联邦学习数学公式纯手推