您的位置 首页 新闻

再见 SharedPreferences,拥抱 Jetpack DataStore

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

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 都有那些坑。

1

那些年我们所经历的 SharedPreferences 坑

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

  • 通过 getXXX 方法获取数据,可能会导致主线程阻塞

  • SharedPreference 不能保证类型安全

  • SharedPreference 加载的数据会一直留在内存中,浪费内存

  • apply 方法虽然是异步的,可能会发生 ANR,在 8.0 之前和 8.0 之后实现各不相同

  • apply 方法无法获取到操作成功或者失败的结果

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

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

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

valsp = getSharedPreferences( “ByteCode”, Context.MODE_PRIVATE) // 异步加载 SP 文件内容

sp.getString( “jetpack”, “”); // 等待 SP 加载完毕

调用 getSharedPreferences 方法,最终会调用 SharedPreferencesImpl#startLoadFromDisk 方法开启一个线程异步读取数据。

frameworks/base/core/java/android/app/SharedPreferencesImpl.java

privatefinalObject mLock = newObject;

privatebooleanmLoaded = false;

privatevoidstartLoadFromDisk{

synchronized(mLock) {

mLoaded = false;

}

newThread( “SharedPreferencesImpl-load”) {

publicvoidrun{

loadFromDisk;

}

}.start;

}

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

public StringgetString( Stringkey, @NullableStringdefValue) {

synchronized (mLock) {

awaitLoadedLocked;

Stringv = ( String)mMap. get(key);

returnv != null? v : defValue;

}

}

private voidawaitLoadedLocked {

……

while(!mLoaded) {

try{

mLock.wait;

} catch(InterruptedException unused) {

}

}

……

}

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

SP 不能保证类型安全

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

valkey = “jetpack”

valsp = 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.Integercannotbecasttojava.lang.String

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

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

// 调用 getSharedPreferences 方法,最后会调用 getSharedPreferencesCacheLocked 方法

publicSharedPreferences getSharedPreferences( File file, intmode ) {

……

final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked;

returnsp;

}

// 通过静态的 ArrayMap 缓存 SP 加载的数据

privatestaticArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

// 将数据保存在 sSharedPrefsCache 中

privateArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked{

……

ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache. get(packageName);

if(packagePrefs == null) {

packagePrefs = newArrayMap<>;

sSharedPrefsCache.put(packageName, packagePrefs);

}

returnpackagePrefs;

}

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

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

再见 SharedPreferences,拥抱 Jetpack DataStore

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

frameworks/base/core/java/android/app/SharedPreferencesImpl.java

publicvoidapply{

finallongstartTime = System.currentTimeMillis;

finalMemoryCommitResult mcr = commitToMemory;

finalRunnable awaitCommit = newRunnable {

@Override

publicvoidrun{

mcr.writtenToDiskLatch.await; // 等待

……

}

};

// 将 awaitCommit 添加到队列 QueuedWork 中

QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = newRunnable {

@Override

publicvoidrun{

awaitCommit.run;

QueuedWork.removeFinisher(awaitCommit);

}

};

// 8.0 之前加入到一个单线程的线程池中执行

// 8.0 之后加入 HandlerThread 中执行写入任务

SharedPreferencesImpl. this.enqueueDiskWrite(mcr, postWriteRunnable);

}

  • 将一个 awaitCommit 的 Runnable 任务,添加到队列 QueuedWork 中,在 awaitCommit 中会调用 await 方法等待,在 handleStopService 、 handleStopActivity 等等生命周期会以这个作为判断条件,等待任务执行完毕

  • 将一个 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

privatevoidenqueueDiskWrite( finalMemoryCommitResult mcr,

finalRunnable postWriteRunnable) {

……

QueuedWork.singleThreadExecutor.execute(writeToDiskRunnable);

}

// android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java

publicstaticExecutorService singleThreadExecutor{

synchronized(QueuedWork.class) {

if(sSingleThreadExecutor == null) {

sSingleThreadExecutor = Executors.newSingleThreadExecutor;

}

returnsSingleThreadExecutor;

}

}

通过 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

privatevoidenqueueDiskWrite( finalMemoryCommitResult mcr,

finalRunnable postWriteRunnable) {

……

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

}

// android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java

privatestaticfinalLinkedList<Runnable> sWork = newLinkedList<>;

publicstaticvoidqueue(Runnable work, booleanshouldDelay) {

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 方法为例。

publicvoidhandlePauseActivity(IBinder token, booleanfinished, booleanuserLeaving,

intconfigChanges, 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

privatestaticfinalConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =

newConcurrentLinkedQueue<Runnable>;

publicstaticvoidwaitToFinish{

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

privatestaticfinalLinkedList<Runnable> sFinishers = newLinkedList<>;

publicstaticvoidwaitToFinish{

……

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 参数作为结果,来看一下它们的方法签名。

publicvoidapply{ … }

publicbooleancommit{ … }

SP 不能用于跨进程通信

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

valsp = getSharedPreferences( “ByteCode”, Context.MODE_PRIVATE)

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

publicSharedPreferences getSharedPreferences(File file, intmode) {

if((mode & Context.MODE_MULTI_PROCESS) != 0||

getApplicationInfo.targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {

// 重新读取 SP 文件内容

sp.startReloadIfChangedUnexpectedly;

}

returnsp;

}

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

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

2

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。

3

在项目中使用 Preferences DataStore

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

1. 需要添加 Preferences DataStore 依赖

implementationandroidx.datastore:datastore-preferences:1.0.0-alpha01″

2. 构建 DataStore

privatevalPREFERENCE_NAME = “DataStore”

vardataStore: DataStore<Preferences> = context.createDataStore(

name = PREFERENCE_NAME

3. 从 Preferences DataStore 中读取数据

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

valKEY_BYTE_CODE = preferencesKey< Boolean>( “ByteCode”)

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

inlinefun< reifiedT : Any> preferencesKey(name: String) : Preferences.Key<T> {

returnwhen(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 来获取数据

overridefunreadData(key: Preferences. Key< Boolean>) : Flow< Boolean> =

dataStore. data

. catch{

// 当读取数据遇到错误时,如果是 `IOException` 异常,发送一个 emptyPreferences 来重新使用

// 但是如果是其他的异常,最好将它抛出去,不要隐藏问题

if(it isIOException) {

it.printStackTrace

emit(emptyPreferences)

} else{

throwit

}

}.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 写入数据即可。

overridesuspend funsaveData(key: Preferences. Key< Boolean>) {

dataStore.edit { mutablePreferences ->

valvalue = mutablePreferences[key] ?: false

mutablePreferences[key] = !value

}

}

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

4

迁移 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,拥抱 Jetpack DataStore

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

5

相比于 MMKV 有什么不同之处

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

再见 SharedPreferences,拥抱 Jetpack DataStore

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

再见 SharedPreferences,拥抱 Jetpack DataStore

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

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

GitHub 地址:

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

参考文献

https://codelabs.developers.google.com/codelabs/android-preferences-datastore

https://medium.com/androiddevelopers/now-in-android-25-8596a08554d7

https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html

https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247484387&idx=1&sn=e3c8d6ef52520c51b5e07306d9750e70&scene=21#wechat_redirect

https://www.jianshu.com/p/3f64caa567e5

本文来自网络,不代表四川资讯网_成都资讯网_成都新闻-中国四川网立场,转载请注明出处:https://www.cnscw.com.cn/104964.html
中国四川网

作者: 中国四川网

为您推荐

发表评论

电子邮件地址不会被公开。 必填项已用*标注

联系我们

联系我们

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部