Jetpack:DataStore必知的几个优点
source link: https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw%3D%3D&%3Bmid=2247484843&%3Bidx=1&%3Bsn=3f41d72f98ddf62f49264b8753bfd331
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
最近 Jetpack
又增加了新成员,提出了一个关于小型数据存储相关的 DataStore
组件。
根据官网的描述, DataStore
完全是对标现有的 SharedPreferences
。
SharedPreferences
相信大家都有用过,既然在现有的基础上提出 DataStore
那自然是为了解决 SharedPreferences
的缺点的。
如果你还不知道 SharedPreferences
有什么缺点?没关系,我们正好来复习一遍。你可以对标一下在使用 SharedPreferences
的过程中是否也遇到过这些问题。
SharedPreferences的糟心事
为了精简语言,下面都将 SharedPreferences
简称 sp
一次性读取阻塞主线程
sp = getSharedPreferences("settings_preference", Context.MODE_PRIVATE)
在使用 sp
的过程中,会通过 getSharedPreferences
来初始化 sp
。
上面这段代码最终会进入 SharedPreferencesImpl
的 loadFromDisk
方法。
具体调用就不带大家走一遍了,如果都贴出来文章就变成代码粘贴板了,我们只关注核心逻辑,其它感兴趣的可以自行查看源码
private void loadFromDisk() { synchronized (mLock) { if (mLoaded) { return; } if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } } // Debugging if (mFile.exists() && !mFile.canRead()) { Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission"); } Map<String, Object> map = null; StructStat stat = null; Throwable thrown = null; try { stat = Os.stat(mFile.getPath()); if (mFile.canRead()) { BufferedInputStream str = null; try { str = new BufferedInputStream( new FileInputStream(mFile), 16 * 1024); map = (Map<String, Object>) XmlUtils.readMapXml(str); } catch (Exception e) { Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { // An errno exception means the stat failed. Treat as empty/non-existing by // ignoring. } catch (Throwable t) { thrown = t; } synchronized (mLock) { mLoaded = true; mThrowable = thrown; // It's important that we always signal waiters, even if we'll make // them fail with an exception. The try-finally is pretty wide, but // better safe than sorry. try { if (thrown == null) { if (map != null) { mMap = map; mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } else { mMap = new HashMap<>(); } } // In case of a thrown exception, we retain the old map. That allows // any open editors to commit and store updates. } catch (Throwable t) { mThrowable = t; } finally { mLock.notifyAll(); } } }
在这里通过对象锁 mLock
机制来对其进行加锁操作。只有当 sp
文件中的数据全部读取完毕之后才会调用 mLock.notifyAll()
来释放锁。
而另一边对应的获取数据的 get
方法,例如 getString
方法
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() { if (!mLoaded) { // Raise an explicit StrictMode onReadFromDisk for this // thread, since the real read will be in a different // thread and otherwise ignored by StrictMode. BlockGuard.getThreadPolicy().onReadFromDisk(); } while (!mLoaded) { try { mLock.wait(); // 等待sp文件读取完毕 } catch (InterruptedException unused) { } } if (mThrowable != null) { throw new IllegalStateException(mThrowable); } }
这里会在 awaitLoadedLocked
方法中调用 mLock.wait()
来等待 sp
的初始化完成。
所以如果 sp
文件过大,初始化所花的时间过多,会导致后面 sp
数据获取时的阻塞。
类型不安全
在我们使用 sp
过程中,用的最多的应该是它的 put
与 get
方法。现在我们用这两个方法来写一段代码
sp = getSharedPreferences("settings_preference", Context.MODE_PRIVATE) // 某一个地方的逻辑 sp.edit().putString("key_name_from_sp", "from sp").apply() // 另一个地方的逻辑 sp.edit().putInt("key_name_from_sp", "from sp").apply() // 获取key_name_from_sp值 sp.getString("key_name_from_sp", "")
如果你运行上面的代码你可以发现程序运行异常,本质问题是对同一个 key
赋值了不同类型的值。将原来 String
类型的值转变成 Int
类型。由于 sp
内部是通过 Map
来保存对于的 key-value
,所以它并不能保证 key-value
的类型固定,也进一步导致通过 get
方法来获取对应 key
的值的类型也是不安全的。这就造成了所谓的类型不安全。
public String getString(String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }
在 getString
的源码中,会进行类型强制转换,如果类型不对就会导致程序崩溃。由于 sp
不会在代码编译时进行提醒,只能在代码运行之后才能发现,所以就避免不掉可能发生的异常,从而导致 sp
类型不安全。
apply异步没有回调
为了防止 sp
写入时阻塞线程,一般都会使用 apply
方法来将数据异步提交到磁盘,即写入到文件中。
虽然 apply
是异步,但它并没有返回值,同样也没有对应的结果回调。
public void apply() { ... }
导致ANR
apply
异步提交解决了线程的阻塞问题,但如果 apply
任务过多数据量过大,可能会导致 ANR
的产生。
ANR
的产生是主线程长时间未响应导致的。 apply
不是异步的吗?它怎么又会产生 ANR
呢?
来看下 apply
的源码
public void apply() { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } if (DEBUG && mcr.wasWritten) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " applied after " + (System.currentTimeMillis() - startTime) + " ms"); } } }; // 注意:将awaitCommit添加到队列中 QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); // 成功写入磁盘之后才将awaitCommit移除 QueuedWork.removeFinisher(awaitCommit); } }; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // Okay to notify the listeners before it's hit disk // because the listeners should always get the same // SharedPreferences instance back, which has the // changes reflected in memory. notifyListeners(mcr); }
这里关键点是会将 awaitCommit
加入到 QueuedWork
队列中,只有当 awaitCommit
执行完之后才会进行移除。
这是一方面,我们再来看另一方面。
在 Activity
的 onPause
与 onStop
、 Service
的 onDestory
中会等待 QueuedWork
中的任务全部完成,一旦 QueuedWork
中的任务非常耗时,例如 sp
的写入磁盘数据量过多,就会导致主线程长时间未响应,从而产生 ANR
。
具体调用分别在 ActivityThread
中的 handlePauseActivity
、 handlePauseActivity
与 handleStopService
方法中。
public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges, PendingTransactionActions pendingActions, String reason) { ActivityClientRecord r = mActivities.get(token); if (r != null) { if (userLeaving) { performUserLeavingActivity(r); } r.activity.mConfigChangeFlags |= configChanges; performPauseActivity(r, finished, reason, pendingActions); // Make sure any pending writes are now committed. if (r.isPreHoneycomb()) { //等待任务完成 QueuedWork.waitToFinish(); } mSomeActivitiesChanged = true; } }
那如何解决呢?首先使用 sp
不要存储过大的 key-value
数据,本身 sp
就是轻量的存储,对于大数据还是使用 room
来存储。
此类 ANR
都是经由 QueuedWork.waitToFinish()
触发的,如果在调用此函数之前,将其中保存的队列手动清空,那么是不是能解决问题呢,答案是肯定的。
另外在今日头条的一篇文章中已经提出解决 ANR
的方法,具体解决可以自行查看
https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247484387&idx=1&sn=e3c8d6ef52520c51b5e07306d9750e70&scene=21#wechat_redirect
不能跨进程通信
sp
是不能跨进程通信的,虽然在获取 sp
的时候提供了 MODE_MULTI_PROCESS
,但内部并不是用来跨进程的。
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
问题不知道你在使用的过程中是否有遇到过,或者说有幸中标几条,大家可以留言来对比一下,说出你的故事(此处应该有酒)。
DataStore
针对 sp
那几个问题, DataStore
都够能规避。为了精简语言,下面都将 DataStore
简 称 ds
。
-
ds kotlin ANR
-
ds sp protocol buffers protocol buffers
-
ds
能够在编译阶段提醒sp
类型错误,保证sp
类型的类型不安全问题。 -
ds Flow Flow
-
ds
完美支持sp
数据的迁移,你可以无成本过渡到ds
。
所 以 ds
将会是 Android
后续轻量数据存储的首选组件。我们也是时候来了解 ds
的使用。
引入DataStore
首先我们要引入 ds
,方式很简单直接在 build
中添加依赖即可。唯一需要注意的是 ds
支持 sp
与 protocol buffers
两种类型,所以对应的也有两种依赖。
// Preferences DataStore implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01" // Proto DataStore implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
下面针对这两种类型分别做介绍。
创建DataStore
针对 sp
类型的数据, ds
只需通过 createDataStore
方法来获取对应的 ds
对象
private val dataStore = createDataStore("settings")
其中 settings
为对应的文件名,存储方式为 datastore/ + name + .preferences_pb
protocol buffers
类型需要额外实现 Serializer
接口,提供读写的入口。
object SettingsSerializer : Serializer<Settings> { override fun readFrom(input: InputStream): Settings { return Settings.parseFrom(input) } override fun writeTo(t: Settings, output: OutputStream) { t.writeTo(output) } } private val dataStoreProto = createDataStore("settings.pb", SettingsSerializer)
其中的 Settings
类是通过 protocol buffers
脚本自动生成的。要生成 Settings
类,你需要做两件事
-
配置
protocol buffers
环境 -
编写
.proto
文件
所以你可能需要懂一点 protocol buffers
相关的语法。
如果后续有空,可能会单独开文章介绍一下 protocol buffers
相关的内容,大厂用的基本上都是 protocol buffers
。
syntax = "proto3"; option java_multiple_files = true; message Settings { string key_name = 1; }
使用 protocol buffers
运行上面的代码就能自动帮我们生成对应的 Settings
类。其中它里面的一个变量就是 keyName_
,它是 String
类型。通过创建类与对应变量的方式来约定类型的安全。
读
sp
与 protocol buffers
类型的读操作使用方式都一样,首先都要创建 Preferences.Key
类型的 key
。
val DATA_KEY = preferencesKey<String>("key_name")
对应的 preferencesKey
如下:
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) } Set::class -> { throw IllegalArgumentException("Use `preferencesSetKey` to create keys for Sets.") } else -> { throw IllegalArgumentException("Type not supported: ${T::class.java}") } } }
它支持 Int
、 String
、 Boolean
、 Float
与 Long
类型的数据,另外还有一个 preferencesSetKey
,用来支持 set
类型的数据。
调用 preferencesKey
每次都创建一个 Preferences.Key
对象,那它这样如何保证是同一个 key
呢?
如果你去看源码就会一目了然。
internal constructor(val name: String) { override fun equals(other: Any?) = if (other is Key<*>) { name == other.name } else { false } override fun hashCode(): Int { return name.hashCode() } }
原来是它重写了 equals
方法,内部实现对 name
的比较。那么只要创建 preferencesKey
时传入的 name
相同,就能保证获取到的是同一个 key
的数据。
有了 key
,再来通过 dataStore.data.map
来获取 Flow
,同时暴露出对应的 Preferences
private suspend fun read() { dataStore.data.map { // unSafe type if (it[DATA_KEY] is String) { it[DATA_KEY] ?: "" } else { "type is String: ${it[DATA_KEY] is String}" } }.collect { Toast.makeText(this@DataStoreActivity, "read result: $it", Toast.LENGTH_LONG).show() } }
同时在 read
中写了一个验证 SharedPreference
类型不安全的示例。如果在别的地方赋值了 DATA_KEY
非 String
类型的数据时,将会弹出 else
中的语句。
下面是 protocol buffers
的读取
private suspend fun protoRead() { dataStoreProto.data.map { // safe type it.keyName }.collect { Toast.makeText(this, "read result success form proto: $it", Toast.LENGTH_LONG).show() } }
需要注意的是这里获取的数据就是类型安全的。这里的 it
对应的就是在 ds
创建时产生的 Settings
。
写
写 sp
与 protocol buffers
有所不同。
对于 sp
直接使用 dataStore.edit
来写入数据
private suspend fun write(value: String) { dataStore.edit { it[DATA_KEY] = value LogUtils.d("dataStore write: $value") } }
而 protocol buffers
使用的是 updateData
方法
private suspend fun protoWrite(value: String) { dataStoreProto.updateData { it.toBuilder().setKeyName(value).build() } }
迁移SharedPreferences
迁移也分为两种,一种是迁移到 ds
的 sp
中;另一种是迁移到 protocol buffers
中。
具体来看,如果迁移到 ds
的 sp
中,只需在之前创建 ds
基础上额外再加一个 migrations
参数。
private val dataStore = createDataStore("settings", migrations = listOf(SharedPreferencesMigration(this, "settings_preference")))
通过创建 SharedPreferencesMigration
来迁移对应的 sp
数据。
下面是迁移到 protocol buffers
中
val settingsDataStore: DataStore<Settings> = context.createDataStore( produceFile = { File(context.filesDir, "settings.preferences_pb") }, serializer = SettingsSerializer, migrations = listOf( SharedPreferencesMigration( context, "settings_preferences" ) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences -> // Map your sharedPrefs to your type here } ) )
迁移完之后需要执行一次代码,同时应该停止再次使用 sp
。如果迁移成功将会删除之前 sp
的 .xml
类型的文件,生成对应 ds
文件。
最后附上一张 Google
分析的 SharedPreferences
和 DataStore
的区别图
目前可以看到 DataStore
还处在 alpha
版本,非常期待它之后的正式版本。
另外,针对 DataStore
的使用,我写了一个 demo
,大家 可以在 android-api-analysis
中 获取。
https://github.com/idisfkj/android-api-analysis
你也可以点击阅读原文查看。
推荐阅读
原创不易,感谢支持
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK