Jetpack Preferences DataStore

1,785 阅读5分钟

Google Jetpack 新出的DataStore用来代替SharePreferences的使用,DataStore有两种实现方式,一种是Preferences DataStore,一种是Proto DataStore。下面文章内容先介绍第一种Preferences DataStore的使用,并会记录使用过程中遇到的坑,以及讲解为何会使用DataStore替换SharePreferences。

第二篇Proto DataStore讲解
Github的项目地址

一:添加依赖

新建项目并在app的build.gradle文件里添加依赖

implementation "androidx.datastore:datastore-preferences:1.0.0-alpha03"
implementation "androidx.datastore:datastore-preferences-core:1.0.0-alpha03"

二:遇到的坑

1 报错 Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper '-jvm-target' option

解决方案:在app的build.gradle里的android的地方添加jvm 1.8的声明如下代码

android {
    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

2 报错 Duplicate class kotlinx.coroutines.AbstractCoroutine found in modules jetified-kotlinx-coroutines-core-jvm-1.3.9.jar

解决方案:去掉datastore-preferences库里的协程,自己添加协程的引用,由于DataStore是依赖于使用协程的,所以需要添加协程

implementation('androidx.datastore:datastore-preferences:1.0.0-alpha03') {
      exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core'
      exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core-jvm'
}
implementation('androidx.datastore:datastore-preferences-core:1.0.0-alpha03') {
    exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core'
    exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core-jvm'
}
// 依赖协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
// 依赖当前平台所对应的平台库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"

kotlin_coroutines是在project的build.gradle里的buildscript下添加ext.kotlin_coroutines = '1.3.9'

3 报错 java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/datastore/preferences/PreferencesProto$PreferenceMap;

解决方案:由于alpha03缺失了PreferencesProto$PreferenceMap这个类,这个问题再alpha04已经解决,所以修改依赖为如下:

implementation('androidx.datastore:datastore-preferences:1.0.0-alpha04') {
    exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core'
    exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core-jvm'
}
implementation('androidx.datastore:datastore-preferences-core:1.0.0-alpha04') {
    exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core'
    exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core-jvm'
}

4 期间遇到资源加载过慢的话,可以在project的build.gradle添加阿里镜像,如下

repositories {
     maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/' }
     google()
     jcenter()
     maven { url "https://jitpack.io" }
     maven { url 'http://maven.aliyun.com/nexus/content/repositories/releases/' }
}

三:混淆的添加

使用DataStore的Preferences DataStore需要在proguard-rules.pro下添加如下代码

-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
    <fields>;
}

使用协程需要在proguard-rules.pro下添加如下代码

# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {}
-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {}

# Most of volatile fields are updated with AFU and should not be mangled
-keepclassmembernames class kotlinx.** {
    volatile <fields>;
}

四:使用

1:创建datastore

// 指定名字
private val PREFERENCES_NAME = "Preferences_DataStore"
// 创建dataStore
var dataStore:DataStore<Preferences> = context.createDataStore(
    name = PREFERENCES_NAME
)

2:存储

// 按存储boolean为例
// 声明一个key
val KEY_FLAG = preferencesKey<Boolean>("key_flag")
// 存储
dataStore.edit { it[KEY_FLAG] = true }

3:获取

// 按存储boolean为例
dataStore.data.map { it[KEY_FLAG]?:false }.first()

4:支持SharePreferences 直接转 DataStore

// SpUtils.SHARE_PREFERENCES_NAME 为创建SP时候使用的名字,
// val sp = context.getSharedPreferences(SHARE_PREFERENCES_NAME, Context.MODE_PRIVATE)
// val SHARE_PREFERENCES_NAME = "share_preferences_name"

dataStore = context.createDataStore(
     name = PREFERENCES_NAME,
     migrations = listOf(
     	SharedPreferencesMigration(
           context,
           SpUtils.SHARE_PREFERENCES_NAME
        )
    )
)

5:关键的代码

// 首先是定义了一个IDataStore的接口类
interface IDataStore {

    suspend fun putBoolean(key:Preferences.Key<Boolean>,value:Boolean)
    suspend fun getBoolean(key:Preferences.Key<Boolean>):Boolean

    suspend fun putInt(key: Preferences.Key<Int>,value:Int)
    suspend fun getInt(key: Preferences.Key<Int>):Int

    suspend fun putLong(key: Preferences.Key<Long>,value:Long)
    suspend fun getLong(key: Preferences.Key<Long>):Long

    suspend fun putFloat(key: Preferences.Key<Float>,value:Float)
    suspend fun getFloat(key: Preferences.Key<Float>):Float

    suspend fun putDouble(key: Preferences.Key<Double>,value: Double)
    suspend fun getDouble(key: Preferences.Key<Double>): Double

    suspend fun putString(key: Preferences.Key<String>,value:String)
    suspend fun getString(key: Preferences.Key<String>):String

    fun spToDataStore()

}

// 其次是一个实现类
class PreferencesDataStore(val context:Application) : IDataStore {

    // 指定名字
    private val PREFERENCES_NAME = "prefs_datastore"
    // 创建dataStore
    var dataStore:DataStore<Preferences> = context.createDataStore(
        name = PREFERENCES_NAME
    )

    override suspend fun putBoolean(key: Preferences.Key<Boolean>,value:Boolean) {
        dataStore.edit { it[key] = value }
    }

    override suspend fun getBoolean(key: Preferences.Key<Boolean>): Boolean{
        return dataStore.data.map { it[key]?:false }.first()
    }

    override suspend fun putInt(key: Preferences.Key<Int>,value:Int) {
        dataStore.edit { it[key] = value }
    }

    override suspend fun getInt(key: Preferences.Key<Int>): Int{
        return dataStore.data.map { it[key]?:0 }.first()
    }

    override suspend fun putLong(key: Preferences.Key<Long>,value:Long) {
        dataStore.edit { it[key] = value }
    }

    override suspend fun getLong(key: Preferences.Key<Long>): Long{
        return dataStore.data.map { it[key]?:0L }.first()
    }

    override suspend fun putDouble(key: Preferences.Key<Double>, value: Double) {
        dataStore.edit { it[key] = value }
    }

    override suspend fun getDouble(key: Preferences.Key<Double>): Double{
        return dataStore.data.map { it[key]?:0.0 }.first()
    }

    override suspend fun putFloat(key: Preferences.Key<Float>,value:Float) {
        dataStore.edit { it[key] = value }
    }

    override suspend fun getFloat(key: Preferences.Key<Float>): Float {
        return dataStore.data.map { it[key]?:0F }.first()
    }

    override suspend fun putString(key: Preferences.Key<String>,value:String) {
        dataStore.edit { it[key] = value }
    }

    override suspend fun getString(key: Preferences.Key<String>): String {
        return dataStore.data.map { it[key]?:"" }.first()
    }

    override fun spToDataStore() {
        /**
         *  传入 migrations 参数,构建一个 DataStore 之后
         *  需要执行 一次读取 或者 写入,DataStore 才会自动合并 SharedPreference 文件内容
         */
        dataStore = context.createDataStore(
            name = PREFERENCES_NAME,
            migrations = listOf(
                SharedPreferencesMigration(
                    context,
                    SpUtils.SHARE_PREFERENCES_NAME
                )
            )
        )
    }

}

// 而外一个存储 DataStore的Key的类
object PreferencesKeys {
    val KEY_COUNT = preferencesKey<Int>("key_count")
    val KEY_FLAG = preferencesKey<Boolean>("key_flag")
    val KEY_PRICE = preferencesKey<Float>("key_price")
    val KEY_NAME = preferencesKey<String>("key_name")
    val KEY_TIME = preferencesKey<Long>("key_time")
    val KEY_MONEY = preferencesKey<Double>("key_money")
}

// 还有一个工厂类,去生成PreferencesDataStore的实例
object StoreFactory {
    @JvmStatic
    fun providePreferencesDataStore(context: Application):IDataStore{
        return PreferencesDataStore(context)
    }
}

// 接着是MainActivity里面的调用
class MainActivity : AppCompatActivity() ,View.OnClickListener ,CoroutineScope by MainScope(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tv_preferences_data_store.setOnClickListener(this)
        tv_sp_to_datastore.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when(v){
            tv_preferences_data_store->{
                launch (Dispatchers.Main){
                    StoreFactory.providePreferencesDataStore(application).putFloat(PreferencesKeys.KEY_PRICE,20F)
                    StoreFactory.providePreferencesDataStore(application).putBoolean(PreferencesKeys.KEY_FLAG,true)
                }
            }
            tv_sp_to_datastore->{
                launch (Dispatchers.Main){
                    StoreFactory.providePreferencesDataStore(application).spToDataStore()
                }
            }
        }
    }

    override fun onDestroy() {
        cancel()
        super.onDestroy()
    }
}

五:为何要用DataStore替代SharePreferences,SharePreferences有哪些坑?

1:SharePreferences的getXXX()方法存在阻塞的隐患

创建Sp的代码 context.getSharedPreferences(SHARE_PREFERENCES_NAME, Context.MODE_PRIVATE) 实际上是调用了ContextImpl的getSharedPreferences方法,代码如下:

 @Override
 public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
                if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                    if (isCredentialProtectedStorage()
                            && !getSystemService(UserManager.class)
                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                        throw new IllegalStateException("SharedPreferences in credential encrypted "
                                + "storage are not available until after user is unlocked");
                    }
                }
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            // If somebody else (some other process) changed the prefs
            // file behind our back, we reload it.  This has been the
            // historical (if undocumented) behavior.
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
}
// 可以看到关键代码 sp = new SharedPreferencesImpl(file, mode); new了一个SharedPreferencesImpl,SharedPreferencesImpl的构造器如下
SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
}
startLoadFromDisk() 方法如下:
private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
}

会发现这里使用到一个锁 mLock,而在SharedPreferences.getXXX()方法里需要等待mLock 锁释放,才能使用,否则就阻塞,所以如果调用完getSharedPreferences马上调用在SharedPreferences.getXXX(),那么会存在阻塞 代码如下:

public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
}

2:SharedPreferences 加载的数据,会一直存在内存当中

上面的getSharedPreferences方法里可以看到,有个final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();这里就会发现通过方法getSharedPreferences加载的数据,最后会将数据存储在静态的成员变量中,通过静态的 ArrayMap 缓存每一个 SP 文件,而每个 SP 文件内容通过 Map 缓存键值对数据,这样数据会一直留在内存中,浪费内存

// 调用 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;
}

3:SharedPreferences 类型不安全,同样的Key,不同的类型可以覆盖

val key = "ccm"
val sp = getSharedPreferences("ccm_test", Context.MODE_PRIVATE) // 异步加载 SP 文件内容

sp.edit { putInt(key, "dddd") }//先使用String类型赋值key
sp.edit { putInt(key, 0) } // 再使用 Int 类型的数据覆盖相同的 key
sp.getString(key, ""); // 使用相同的 key 读取 Sting 类型的数据

上面的代码,对key存入String值,再同样的key用Int去覆盖,如果有地方用该Key去获取String值,那么就会报错

4:SharedPreferences 不能跨进程通信

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 来做跨进程通信

5:apply() 方法可能导致ANR

字节跳动的ANR例子

上面的链接是字节跳动曾经遇到的ANR问题

六:DataStore解决了什么问题?

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

七:Preferences DataStore跟SharePreferences存储文件地址区分

我们可以使用adb shell
run-as 包名
ls
查看到files文件夹跟shared-prefs文件夹
sp的文件就存放在shared-prefs,而datastore的文件就存放在files的datastore文件夹里