共计 16259 个字符,预计需要花费 41 分钟才能阅读完成。
/ DataStore 介绍 /
Jetpack DataStore 是一种改良的新数据存储解决方案,容许应用协定缓冲区存储键值对或类型化对象。
DataStore 以异步、统一的事务形式存储数据,克服了 SharedPreferences(以下统称为 SP)的一些毛病。
DataStore 基于 Kotlin 协程和 Flow 实现,并且能够对 SP 数据进行迁徙,旨在取代 SP。
DataStore 提供了两种不同的实现:Preferences DataStore 与 Proto DataStore,其中 Preferences DataStore 用于存储键值对;Proto DataStore 用于存储类型化对象,前面会别离给出对应的应用例子。
/ SharedPreferences 毛病 /
DataStore 呈现之前,咱们用的最多的存储形式毫无疑问是 SP,其应用形式简略、易用,广受好评。然而 google 对 SP 的定义为轻量级存储,如果存储的数据少,应用起来没有任何问题,当须要存储数据比拟多时,SP 可能会导致以下问题:
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()函数:
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); | |
public void apply() {final MemoryCommitResult mcr = commitToMemory(); | |
final Runnable awaitCommit = new Runnable() {public void run() { | |
try {mcr.writtenToDiskLatch.await(); | |
} catch (InterruptedException ignored) {}} | |
}; | |
//8.0 之前 | |
QueuedWork.add(awaitCommit); | |
//8.0 之后 | |
QueuedWork.addFinisher(awaitCommit); | |
// 异步执行磁盘写入操作 | |
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); | |
//...... 其余...... | |
} |
结构一个名为 awaitCommit 的 Runnable 工作并将其退出到 QueuedWork 中,该工作外部间接调用了 CountDownLatch.await()办法,即间接在 UI 线程执行期待操作,那么须要看 QueuedWork 中何时执行这个工作。
QueuedWork 类在 Android8.0 以上和 8.0 以下的版本实现形式有区别:
8.0 之前 QueuedWork.java:
public class QueuedWork { | |
private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = | |
new ConcurrentLinkedQueue<Runnable>(); | |
public static void add(Runnable finisher) {sPendingWorkFinishers.add(finisher); | |
} | |
public static void waitToFinish() { | |
Runnable toFinish; | |
// 从队列中取出工作:如果工作为空,则跳出循环,UI 线程能够持续往下执行;// 反之工作不为空,取出工作并执行,理论执行的 CountDownLatch.await(),即 UI 线程会阻塞期待 | |
while ((toFinish = sPendingWorkFinishers.poll()) != null) {toFinish.run(); | |
} | |
} | |
//...... 其余...... | |
} |
8.0 之后 QueuedWork.java:
public class QueuedWork {private static final LinkedList<Runnable> sFinishers = new LinkedList<>(); | |
public static void waitToFinish() {Handler handler = getHandler(); | |
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); | |
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;} | |
} | |
} |
能够看到不论 8.0 之前还是之后,waitToFinish()都会尝试从 Runnable 工作队列中取工作,如果有的话间接取出并执行,间接看哪里调用了 waitToFinish():
ActivityThread.java
private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) { | |
//...... 其余...... | |
QueuedWork.waitToFinish();} | |
private void handleStopService(IBinder token) { | |
//...... 其余...... | |
QueuedWork.waitToFinish();} |
省略了一些代码细节,能够看到在 ActivityThread 中 handleStopActivity、handleStopService 办法中都会调用 waitToFinish()办法,即在 Activity 的 onStop()中、Service 的 onStop()中都会先同步期待写入工作实现才会继续执行。
所以 apply()尽管是异步写入磁盘,然而如果此时执行到 Activity/Service 的 onStop(),仍然可能会阻塞 UI 线程导致 ANR。
画外音:SP 应用过程中导致的 ANR 问题,能够通过一些 Hook 伎俩进行优化,如字节公布的 今日头条 ANR 优化实际系列 – 辞别 SharedPreference 期待(https://mp.weixin.qq.com/s/kf…)。我司我的项目里应用的 SP 也是按此优化,优化后成果还是比较显著的,所以目前我的项目也还没有对 SP 进行迁徙(如迁徙到 MMKV 或 DataStore),但并不影响咱们学习新的存储姿态。
/ DataStore 应用 /
DataStore 劣势:
- DataStore 基于事务形式解决数据更新。
- DataStore 基于 Kotlin Flow 存取数据,默认在 Dispatchers.IO 里异步操作,防止阻塞 UI 线程,且在读取数据时能对产生的 Exception 进行解决。
- 不提供 apply()、commit()存留数据的办法。
- 反对 SP 一次性主动迁徙至 DataStore 中。
Preferences DataStore
增加依赖项
implementation 'androidx.datastore:datastore-preferences:1.0.0'
构建 Preferences DataStore
val Context.bookDataStorePf: DataStore<Preferences> by preferencesDataStore( | |
// 文件名称 | |
name = "pf_datastore") |
通过下面的代码,咱们就胜利创立了 Preferences DataStore,其中 preferencesDataStore()是一个顶层函数,蕴含以下几个参数:
- name:创立 Preferences DataStore 的文件名称。
- corruptionHandler:如果 DataStore 在试图读取数据时,数据无奈反序列化,会抛出 androidx.datastore.core.CorruptionException,此时会执行 corruptionHandler。
- produceMigrations:SP 产生迁徙到 Preferences DataStore。ApplicationContext 作为参数传递给这些回调,迁徙在对数据进行任何拜访之前运行。
- scope:协程作用域,默认 IO 操作在 Dispatchers.IO 线程中执行。
上述代码执行后,会在 /data/data/ 我的项目包名 /files/ 下创立名为 pf_datastore 的文件如下:
能够看到后缀名并不是 xml,而是.preferences_pb。这里须要留神一点:不能将下面的初始化代码写到 Activity 外面去,否则反复进入 Actvity 并应用 Preferences DataStore 时,会尝试去创立一个同名的.preferences_pb 文件,因为之前曾经创立过一次,当检测到尝试创立同名文件时,会间接抛异样:java.lang.IllegalStateException: There are multiple DataStores active for the same file:xxx.You should either maintain your DataStore as a singleton or confirm that there is no two DataStore’s active on the same file (by confirming that the scope is cancelled).报错类在 androidx.datastore:datastore-core:1.0.0 的 androidx/datastore/core/SingleProcessDataStore 下:
internal val activeFiles = mutableSetOf<String>() | |
file.absolutePath.let {synchronized(activeFilesLock) {check(!activeFiles.contains(it)) { | |
"There are multiple DataStores active for the same file: $file. You should" + | |
"either maintain your DataStore as a singleton or confirm that there is" + | |
"no two DataStore's active on the same file (by confirming that the scope"+" is cancelled)." | |
} | |
activeFiles.add(it) | |
} | |
} |
其中 file 是通过 File(applicationContext.filesDir, “datastore/$fileName”)生成的文件,即 Preferences DataStore 最终要在磁盘中操作的文件地址,activeFiles 是在内存中保留生成的文件门路的,如果判断到 activeFiles 里曾经有该文件,间接抛异样,即不容许反复创立。
-
存数据
首先申明一个实体类 BookModel:
data class BookModel( | |
var name: String = "", | |
var price: Float = 0f, | |
var type: Type = Type.ENGLISH | |
) | |
enum class Type { | |
MATH, | |
CHINESE, | |
ENGLISH | |
} |
BookRepo.kt 中执行存储操作:
const val KEY_BOOK_NAME = "key_book_name" | |
const val KEY_BOOK_PRICE = "key_book_price" | |
const val KEY_BOOK_TYPE = "key_book_type" | |
//Preferences.Key<T> 类型 | |
object PreferenceKeys {val P_KEY_BOOK_NAME = stringPreferencesKey(KEY_BOOK_NAME) | |
val P_KEY_BOOK_PRICE = floatPreferencesKey(KEY_BOOK_PRICE) | |
val P_KEY_BOOK_TYPE = stringPreferencesKey(KEY_BOOK_TYPE) | |
} | |
/** | |
* Preferences DataStore 存数据 | |
*/ | |
suspend fun saveBookPf(book: BookModel) { | |
context.bookDataStorePf.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 | |
} | |
} |
Activity 中:
lifecycleScope.launch { | |
val book = BookModel( | |
name = "Hello Preferences DataStore", | |
price = (1..10).random().toFloat(), // 这里价格每次点击都会变动,为了展现 UI 层能随时监听数据变动 | |
type = Type.MATH ) | |
mBookRepo.savePfData(book) | |
} |
通过 bookDataStorePf.edit(transform: suspend (MutablePreferences) -> Unit) 挂起函数进行存储,该函数承受 transform 块,可能以事务形式更新 DataStore 中的状态。
-
取数据
/** | |
* Preferences DataStore 取数据 取数据时能够对 Flow 数据进行一系列解决 | |
*/ | |
val bookPfFlow: Flow<BookModel> = context.bookDataStorePf.data.catch { exception -> | |
// dataStore.data throws an IOException when an error is encountered when reading data | |
if (exception is IOException) {emit(emptyPreferences()) | |
} else {throw exception} | |
}.map { preferences -> | |
// 对应的 Key 是 Preferences.Key<T> | |
val bookName = 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 BookModel(bookName, bookPrice, bookType) | |
} |
Activity 中:
lifecycleScope.launch { | |
mBookViewModel.bookPfFlow.collect {mTvContentPf.text = it.toString() | |
} | |
} |
通过 bookDataStorePf.data 返回的是 Flow<BookModel>,那么后续就能够通过 Flow 对数据进行一系列解决。从文件读取数据时,如果呈现谬误,零碎会抛出 IOExceptions。能够在 map() 之前应用 catch() 运算符,并且在抛出的异样是 IOException 时收回 emptyPreferences()。如果呈现其余类型的异样,从新抛出该异样。
留神:Preferences DataStore 存取数据时的 Key 是 Preferences.Key< T> 类型,且其中的 T 只能存 Int、Long、Float、Double、Boolean、String、Set< String> 类型,此限度在 androidx/datastore/preferences/core/PreferencesSerializer 类参加序列化的 getValueProto()办法中:
private fun getValueProto(value: Any): Value {return when (value) {is Boolean -> Value.newBuilder().setBoolean(value).build() | |
is Float -> Value.newBuilder().setFloat(value).build() | |
is Double -> Value.newBuilder().setDouble(value).build() | |
is Int -> Value.newBuilder().setInteger(value).build() | |
is Long -> Value.newBuilder().setLong(value).build() | |
is String -> Value.newBuilder().setString(value).build() | |
is Set<*> -> | |
@Suppress("UNCHECKED_CAST") | |
Value.newBuilder().setStringSet(StringSet.newBuilder().addAllStrings(value as Set<String>) | |
).build() | |
// 如果不是下面的类型,会间接抛异样 | |
else -> throw IllegalStateException("PreferencesSerializer does not support type: ${value.javaClass.name}" | |
) | |
} | |
} |
能够看到最初一个 else 逻辑中,如果不是下面的类型,会间接抛异样。因为 Key 是 Preferences.Key< T> 类型,零碎默认帮咱们包了一层,位于 androidx.datastore.preferences.core.PreferencesKeys.kt:
public fun intPreferencesKey(name: String): Preferences.Key<Int> = Preferences.Key(name) | |
public fun doublePreferencesKey(name: String): Preferences.Key<Double> = Preferences.Key(name) | |
public fun stringPreferencesKey(name: String): Preferences.Key<String> = Preferences.Key(name) | |
public fun booleanPreferencesKey(name: String): Preferences.Key<Boolean> = Preferences.Key(name) | |
public fun floatPreferencesKey(name: String): Preferences.Key<Float> = Preferences.Key(name) | |
public fun longPreferencesKey(name: String): Preferences.Key<Long> = Preferences.Key(name) | |
public fun stringSetPreferencesKey(name: String): Preferences.Key<Set<String>> = | |
Preferences.Key(name) |
因为上述的申明都在顶层函数中,所以能够间接应用。比方咱们想申明一个 String 类型的 Preferences.Key< T>,能够间接如下进行申明:
val P_KEY_NAME: Preferences.Key<String> = stringPreferencesKey("key")
-
SP 迁徙至 Preferences DataStore
如果想对 SP 进行迁徙,只需在 Preferences DataStore 构建环节增加 produceMigrations 参数(该参数含意创立环节已介绍)如下:
//SharedPreference 文件名 | |
const val BOOK_PREFERENCES_NAME = "book_preferences" | |
val Context.bookDataStorePf: DataStore<Preferences> by preferencesDataStore( | |
name = "pf_datastore", //DataStore 文件名称 | |
// 将 SP 迁徙到 Preference DataStore 中 | |
produceMigrations = { context -> | |
listOf(SharedPreferencesMigration(context, BOOK_PREFERENCES_NAME)) | |
} | |
) |
这样构建实现时,SP 中的内容也会迁徙到 Preferences DataStore 中了,留神迁徙是一次性的,即执行迁徙后,SP 文件会被删除,如下:
Proto DataStore
SP 和 Preferences DataStore 的一个毛病是无奈定义架构,保障不了存取键时应用了正确的数据类型。Proto DataStore 可利用 Protocol Buffers 协定缓冲区(https://developers.google.com…)定义架构来解决此问题。Protobuf 协定缓冲区是一种对结构化数据进行序列化的机制。通过应用协定,Proto DataStore 能够晓得存储的类型,无需应用键便能提供类型。
- 增加依赖项
1、增加协定缓冲区插件及 Proto DataStore 依赖项 为了应用 Proto DataStore,让协定缓冲区为咱们的架构生成代码,须要在 build.gradle 中引入 protobuf 插件:
plugins { | |
... | |
id "com.google.protobuf" version "0.8.17" | |
} | |
android { | |
//............. 其余配置.................. | |
sourceSets { | |
main {java.srcDirs = ['src/main/java'] | |
proto { | |
// 指定 proto 源文件地址 | |
srcDir 'src/main/protobuf' | |
include '**/*.protobuf' | |
} | |
} | |
} | |
//proto buffer 协定缓冲区相干配置 用于 DataStore | |
protobuf { | |
protoc { | |
//protoc 版本参见:https://repo1.maven.org/maven2/com/google/protobuf/protoc/ | |
artifact = "com.google.protobuf:protoc:3.18.0" | |
} | |
// Generates the java Protobuf-lite code for the Protobufs in this project. See | |
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation | |
// for more information. | |
generateProtoTasks {all().each { task -> | |
task.builtins { | |
java {option 'lite'} | |
} | |
} | |
} | |
// 批改生成 java 类的地位 默认是 $buildDir/generated/source/proto | |
generatedFilesBaseDir = "$projectDir/src/main/generated" | |
} | |
} | |
dependencies { | |
api 'androidx.datastore:datastore:1.0.0' | |
api "com.google.protobuf:protobuf-javalite:3.18.0" | |
... | |
} |
须要配置或引入的库看上去还挺多,能够思考将这些配置独自放到一个 module 中去。
2、定义和应用 protobuf 对象
只需对数据结构化的形式进行一次定义,编译器便会生成源代码,轻松写入和读取结构化数据。咱们是配置依赖项的 sourceSets{}中申明了 proto 源码地址门路在 src/main/protobuf,所有的 proto 文件都要在该申明的门路下:
Book.proto 文件内容:
// 指定 protobuf 版本,没有指定默认应用 proto2,必须在第一行进行指定 | |
syntax = "proto3"; | |
//option:可选字段 | |
//java_package:指定 proto 文件生成的 java 类所在的包名 | |
option java_package = "org.ninetripods.mq.study"; | |
//java_outer_classname:指定该 proto 文件生成的 java 类的名称 | |
option java_outer_classname = "BookProto"; | |
enum Type { | |
MATH = 0; | |
CHINESE = 1; | |
ENGLISH = 2; | |
} | |
message Book { | |
string name = 1; // 书名 | |
float price = 2; // 价格 | |
Type type = 3; // 类型 | |
} |
上述代码编写完后,执行 Build -> ReBuild Project,就会在 generatedFilesBaseDir 配置的门路下生成对应 Java 代码,如下:
3、创立序列化器
序列化器定义了如何存取咱们在 proto 文件中定义的数据类型。如果磁盘上没有数据,序列化器还会定义默认返回值。如下咱们创立一个名为 BookSerializer 的序列化器:
object BookSerializer : Serializer<BookProto.Book> {override val defaultValue: BookProto.Book = BookProto.Book.getDefaultInstance() | |
override suspend fun readFrom(input: InputStream): BookProto.Book { | |
try {return BookProto.Book.parseFrom(input) | |
} catch (exception: InvalidProtocolBufferException) {throw CorruptionException("Cannot read proto.", exception) | |
} | |
} | |
override suspend fun writeTo(t: BookProto.Book, output: OutputStream) {t.writeTo(output) | |
} | |
} |
其中,BookProto.Book 是通过协定缓冲区生成的代码,如果找不到 BookProto.Book 对象或相干办法,能够清理并 Rebuild 我的项目,以确保协定缓冲区生成对象。
-
构建 Proto DataStore
// 构建 Proto DataStore | |
val Context.bookDataStorePt: DataStore<BookProto.Book> by dataStore( | |
fileName = "BookProto.pb", | |
serializer = BookSerializer) |
dataStore 为顶层函数,能够传入的参数如下:
- fileName: 创立 Proto DataStore 的文件名称。
- serializer: Serializer 序列化器定义了如何存取格式化数据。
- corruptionHandler:如果 DataStore 在试图读取数据时,数据无奈反序列化,抛出 androidx.datastore.core.CorruptionException,则调用 corruptionHandler。
- produceMigrations:SP 迁徙到 Proto DataStore 时执行。ApplicationContext 作为参数传递给这些回调,迁徙在对数据进行任何拜访之前运行
- scope:协程作用域,默认 IO 操作在 Dispatchers.IO 线程中执行。
上述代码执行后,会在 /data/data/ 我的项目包名 /files/ 下创立名为 BookProto.pb 的文件如下:
存数据
lifecycleScope.launch { | |
// 构建 BookProto.Book | |
val bookInfo = BookProto.Book.getDefaultInstance().toBuilder() | |
.setName("Hello Proto DataStore") | |
.setPrice(20f) | |
.setType(BookProto.Type.ENGLISH) | |
.build() | |
bookDataStorePt.updateData {bookInfo} | |
} |
Proto DataStore 提供了一个挂起函数 DataStore.updateData() 来存数据,当存储实现时,协程也执行结束。
取数据
/** | |
* Proto DataStore 取数据 | |
*/ | |
val bookProtoFlow: Flow<BookProto.Book> = context.bookDataStorePt.data | |
.catch { exception -> | |
if (exception is IOException) {emit(BookProto.Book.getDefaultInstance()) | |
} else {throw exception} | |
} | |
//Activity 中 | |
lifecycleScope.launch { | |
mBookViewModel.bookProtoFlow.collect {mTvContentPt.text = it.toString() | |
} | |
} |
Proto DataStore 取数据形式跟 Preferences DataStore 一样,不再赘述。
-
SP 迁徙至 Proto DataStore
// 构建 Proto DataStore | |
val Context.bookDataStorePt: DataStore<BookProto.Book> by dataStore( | |
fileName = "BookProto.pb", | |
serializer = BookSerializer, | |
// 将 SP 迁徙到 Proto DataStore 中 | |
produceMigrations = { context -> | |
listOf( | |
androidx.datastore.migrations.SharedPreferencesMigration( | |
context, | |
BOOK_PREFERENCES_NAME | |
) { sharedPrefs: SharedPreferencesView, currentData: BookProto.Book -> | |
// 从 SP 中取出数据 | |
val bookName: String = sharedPrefs.getString(KEY_BOOK_NAME, "") ?:"" | |
val bookPrice: Float = sharedPrefs.getFloat(KEY_BOOK_PRICE, 0f) | |
val typeStr = sharedPrefs.getString(KEY_BOOK_TYPE, BookProto.Type.MATH.name) | |
val bookType: BookProto.Type = | |
BookProto.Type.valueOf(typeStr ?: BookProto.Type.MATH.name) | |
// 将 SP 中的数据存入 Proto DataStore 中 | |
currentData.toBuilder() | |
.setName(bookName) | |
.setPrice(bookPrice) | |
.setType(bookType) | |
.build()} | |
) | |
} | |
) |
Proto DataStore 定义了 SharedPreferencesMigration 类。migrate 里指定了上面两个参数:
- SharedPreferencesView:能够用于从 SharedPreferences 中检索数据
- BookProto.Book:以后数据
同样地在创立时如果传入了 produceMigrations,那么 SP 文件会迁徙至 Proto DataStore,迁徙完后 SP 文件被删除。
这里还须要留神一点,Preferences DataStore、Proto DataStore 在执行迁徙时都会用到 SharedPreferencesMigration 类,然而这两个中央用到该类对应的包名是不一样的,如 Proto DataStore 的包名门路是 androidx.datastore.migrations.SharedPreferencesMigration,当把他们写在一个文件里时,留神其中一个要应用残缺门路。
/ 总结 /
间接上官网给出的 SP 与 DataStore 比照吧:
关注我,每天分享常识干货~