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问题
六:DataStore解决了什么问题?
- DataStore 是基于 Flow 实现的,所以保证了在主线程的安全性
- 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性)
- 没有 apply() 和 commit() 等等数据持久的方法
- 自动完成 SharedPreferences 迁移到 DataStore,保证数据一致性,不会造成数据损坏
- 可以监听到操作成功或者失败结果
七:Preferences DataStore跟SharePreferences存储文件地址区分
我们可以使用adb shell
run-as 包名
ls
查看到files文件夹跟shared-prefs文件夹
sp的文件就存放在shared-prefs,而datastore的文件就存放在files的datastore文件夹里