/   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 DataStoreval 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 DataStoreval 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比照吧:

关注我,每天分享常识干货~