SharedPreferences 很垃圾吗? 嗯,他会阻塞主线程。他可能会崩溃,他可能无法提供大内容的存储,性能比较差,ANR等等。
但是是它的错吗?他的设计本意是提供极少的一些变量存储。结果臃肿的代码和封装写法,过度使用导致了很多问题。
如果不想看全篇,看粗体内容看看你是否有共鸣和不了解的地方,查漏补缺。
目前流行的存储有如下几个我这边自行给出个人使用感受:
-
MMKV
通过内存映射,写入文件就是写入内存,读取和存储都极快。稳定性和速度都能经得起考验。它是腾讯出品用于日志存储的,由于是奔溃瞬间也能记录,那么也就只有mmap的特性,进程死了也能存储。但是会有较低概率,彻底损坏,而且是整个文件损坏。所以存储无关紧要的东西最好。如果真实发生损坏的话,丢失了也没什么关系。但是,很多公司和代码都习惯使用mmkv来作为缓存第一方案。比较概率还是很低很低的。
我有做过上架app的程序,也有在系统平台定制rom开发的程序,mmkv丢失没有遇到过的,但是可能并没有及时同步到文件中(比如改变一个配置,立刻关机,下次开机该配置是老的,修改办法是每次有修改都delay10秒去mmkv.async()一下)。因为它要的就是快,如果每次你写入,立刻保存它就退化成了SharedPref了。那么,极度依赖数据不丢失的程序,建议开启自行编写一个策略去备份,比如10天半个月备份一次,引自官方文档:
javaString backupRootDir = getFilesDir().getAbsolutePath() + "/mmkv_backup_3"; // backup one instance boolean ret = MMKV.backupOneToDirectory(mmapID, backupRootDir, null); // backup all instances long count = MMKV.backupAllToDirectory(backupRootDir); // restore one instance ret = MMKV.restoreOneMMKVFromDirectory(mmapID, backupRootDir, otherDir); // restore all instances count = MMKV.restoreAllFromDirectory(backupRootDir);
那么, 大部分情况,都是比较推荐使用MMKV的。如果你的产品并不在意大部分丢失。比如我们的一个app本身日活可能是每周2次,哪怕是登录信息丢失了。也没什么,用户也会认为过期等再次登录即可。当然是需要你们产品综合考虑的。更何况在一般手机上并不会突然断电,断电也轮不到你的app在工作。
-
SharedPreferences
android有的时候就有它了,基本上代码15年机制没有变化。他的缺点前面已经提到。引用别人的帖子讲的很全:
1、不要存放大的key和value在SharedPreferences中,否则会一直存诸在内存中得不到释放,内存使用过高会频发引发GC,导致界面丢帧甚至ANR。
2、不相关的配置选项最好不要放在一起,单个文件越大读取速速度则越慢。
3、读取频繁的key和不频繁的key尽量不要放在一起(如果整个个文件本身就较小则忽略,为了这点性能添加维护得不偿失)。
4、不要每次都edit,因为每次都会创建一个新的Editorlmpl对象象,最好是批量处理统一提交。
否则edit().commit每次创建一个Editorlmpl对象并且进行一次IO操作,严重影响性能。
5、commit发生在Ul线程中,apply发生在工作线程中,对于数据的提交最好是批量操作统一提交。虽然apply发生在工作线程(不会因为IO阻塞UI线程)但是如果添加任
务较多也有可能带来其他严重后果(参照ActivityThread源码中handleStopActivity方法实现)。
6、尽量不要存放大json,大html,这种可以直接文件缓存。 7、不要指望这货能够跨进程通信Context.PROCESS。
8、最好提前初始化SharedPreferences, 避免SharedPreferences第一次创建时读取文件线程未结束而出现等待情况。
那么我再补充一点,很多人封装了这种static函数:
javapublic static void putXXX(Context ctx, String key, boolean value) { // name存储文件名称 ctx.getSharedPreferences(NAME, Context.MODE_PRIVATE).edit().putXXX(key, value).apply(); } public static boolean getXXX(Context ctx, String key, boolean defValue) { // name存储文件名称 return ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).getXXX(key, defValue); }
对吧,但是看了源码你就知道,如果你频繁需要用到这个函数的时候,你每次去创建内部有大量的同步锁,文件读取,创建等待。性能备受挑战。综合这些SP的问题,其实大部分是使用者给他强加了太多的压力了,他本来就是一个小巧的东西。
经验总结:
1. 一定要存的条目少,value要短;条目多了拆文件。
2. 大量读取少量写入的case,一定要结合static变量,避免每次都新建sp(后面我会贴出参考代码)。
3. Application的attachBaseContext里面如果有读取需要,建议考虑使用SP。MMKV可能初始化没好,DataStore又是异步。 -
DataStore
文件存储跟SP一样,会自行备份,安全不丢失 。
也解决了SP的各种卡顿,隐形奔溃。
缺点是,项目没有kotlin不行。读取必须是suspend,异步。如果非要同步可以使用runBlocking{}包裹。
稍微复杂点,我已经封装好类。
同样的,如果你比如在application或者首屏有加载需要,考虑拆分文件。
SharedPrefUtils静态类: 前面提到过,少读少取就用这种封装类:
AppDataStore单例类:kotlin项目DataStore的单例帮助类:
MemorySharedPreference类:多读少取就使用这种封装类:
java
class MyPefUtil {
private static String perf; //通过这种方式来提速读取效率。因为这个perf在代码中需要到处读取
public static void putPerf(Context ctx, String key, String value) {
this.perf = value;
// name存储文件名称
ctx.getSharedPreferences(NAME, Context.MODE_PRIVATE).edit().putString(key, value).apply();
}
public static String getXXX(Context ctx, String key) {
if (perf != null) {
return perf;
}
// name存储文件名称
perf = ctx.getSharedPreferences(NAME, Context.MODE_PRIVATE).getString(key, defValue);
return perf;
}
}
MemorySharedPreference:
kotlin
class MemorySharedPreference(private val spName:String, private val applicationContext:Context) {
private val sp by lazy { applicationContext.getSharedPreferences(spName, Context.MODE_PRIVATE) }
//暂时内存结果。避免过度read
private val map = ConcurrentHashMap<String?, Any?>(4)
fun getString(key: String, defValue: String?): String? {
if (map.containsKey(key)) {
return map[key].asOrNull()
}
return sp.getString(key, defValue).also { map[key] = it }
}
fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
if (map.containsKey(key)) {
return map[key].asOrNull()
}
return sp.getStringSet(key, defValues).also { map[key] = it }
}
fun getInt(key: String, defValue: Int): Int? {
if (map.containsKey(key)) {
return map[key].asOrNull()
}
return sp.getInt(key, defValue).also { map[key] = it }
}
fun getLong(key: String, defValue: Long): Long {
if (map.containsKey(key)) {
return map[key].asOrNull() ?: 0
}
return sp.getLong(key, defValue).also { map[key] = it }
}
fun getFloat(key: String, defValue: Float): Float {
if (map.containsKey(key)) {
return map[key].asOrNull() ?: 0f
}
return sp.getFloat(key, defValue).also { map[key] = it }
}
fun getBoolean(key: String, defValue: Boolean): Boolean {
if (map.containsKey(key)) {
return map[key].asOrNull() ?: false
}
return sp.getBoolean(key, defValue).also { map[key] = it }
}
fun contains(key: String): Boolean {
if (map.containsKey(key)) {
return true
}
return sp.contains(key)
}
fun putString(key: String, value: String?) {
map[key] = value
sp.edit().putString(key, value).apply()
}
fun putStringSet(key: String, values: MutableSet<String>?) {
map[key] = values
sp.edit().putStringSet(key, values).apply()
}
fun putInt(key: String, value: Int) {
map[key] = value
sp.edit().putInt(key, value).apply()
}
fun putLong(key: String, value: Long) {
map[key] = value
sp.edit().putLong(key, value).apply()
}
fun putFloat(key: String, value: Float) {
map[key] = value
sp.edit().putFloat(key, value).apply()
}
fun putBoolean(key: String, value: Boolean) {
map[key] = value
sp.edit().putBoolean(key, value).apply()
}
fun remove(key: String) {
map.remove(key)
sp.edit().remove(key).apply()
}
fun clear() {
map.clear()
sp.edit().clear().apply()
}
}
java
public class SharedPrefUtil {
private static String SPXMLNAME = "sp_config";
public static void removeKey(Context ctx, String key) {
ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).edit().remove(key).commit();
}
// 1,存储boolean变量方法
public static void putBoolean(Context ctx, String key, boolean value) {
// name存储文件名称
ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).edit().putBoolean(key, value).apply();
}
// 2,读取boolean变量方法
public static boolean getBoolean(Context ctx, String key, boolean defValue) {
// name存储文件名称
return ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).getBoolean(key, defValue);
}
public static void putString(Context ctx, String key, String value) {
// name存储文件名称
ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).edit().putString(key, value).apply();
}
public static String getString(Context ctx, String key, String defValue) {
// name存储文件名称
return ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).getString(key, defValue);
}
//
public static void putInt(Context ctx, String key, int value) {
// name存储文件名称
ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).edit().putInt(key, value).apply();
}
public static int getInt(Context ctx, String key, int defValue) {
// name存储文件名称
return ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).getInt(key, defValue);
}
}
kotlin
object AppDataStore {
private const val DATA_STORE_NAME = "dataStore" //对应最终件:/data/data/xxxx/files/datastore/dataStore.preferences_pb
private later var app:Application
val Context.dataStore by preferencesDataStore(
name = DATA_STORE_NAME,//指定名称
// produceMigrations = {context -> //指定要恢复的sp文件,无需恢复可不写
// listOf(SharedPreferencesMigration(context, SP_PREFERENCES_NAME))
// }
)
suspend inline fun <reified T> containsKey(key:String) : Boolean{
val prefKey = when (T::class.java) {
Int::class.java -> intPreferencesKey(key)
Long::class.java -> longPreferencesKey(key)
Double::class.java -> doublePreferencesKey(key)
Float::class.java -> floatPreferencesKey(key)
Boolean::class.java -> booleanPreferencesKey(key)
String::class.java -> stringPreferencesKey(key)
Set::class.java -> stringSetPreferencesKey(key)
else -> {
throw IllegalArgumentException("This type can be removed from DataStore")
}
}
val t = app.dataStore.data.map {
it.asMap().forEach { (t, u) ->
ALog.t("allData: $t -> $u")
}
it.contains(prefKey)
}.first()
return t
}
fun clear() {
runBlocking {app.dataStore.edit {
ALog.t("clear!")
it.clear()
} }
}
fun save(key:String, value: Any) {
runBlocking {
ALog.t("save $key <to> $value")
saveSuspend(key, value)
}
}
fun save(vararg pair:Pair<String, Any>) {
runBlocking {
pair.forEach {
saveSuspend(it.first, it.second)
}
}
}
inline fun <reified T> remove(key:String) {
runBlocking {
removeSuspend<T>(key)
}
}
suspend inline fun <reified T> removeSuspend(key:String) : T?{
var ret : T? = null
app.dataStore.edit { setting ->
ret = when (T::class.java) {
Int::class.java -> setting.remove(intPreferencesKey(key)).asOrNull()
Long::class.java -> setting.remove(longPreferencesKey(key)).asOrNull()
Double::class.java -> setting.remove(doublePreferencesKey(key)).asOrNull()
Float::class.java -> setting.remove(floatPreferencesKey(key)).asOrNull()
Boolean::class.java -> setting.remove(booleanPreferencesKey(key)).asOrNull()
String::class.java -> setting.remove(stringPreferencesKey(key)).asOrNull()
Set::class.java -> setting.remove(stringSetPreferencesKey(key)).asOrNull() //later: 这里不做二次检查了。默认就认为是stringSet
else -> {
throw IllegalArgumentException("This type can be removed from DataStore")
}
}
}
return ret
}
/**
* 因为我们用于保存,不应该使用lifeCycleScope来发起。有可能无法保存成功。应该使用全局scope。
*/
@Deprecated("不建议直接使用,因为可能协程被取消,除非你明白你的scope一定保存成功")
suspend fun saveSuspend(key:String, value:Any) {
app.dataStore.edit { setting ->
when (value) {
is Int -> setting[intPreferencesKey(key)] = value
is Long -> setting[longPreferencesKey(key)] = value
is Double -> setting[doublePreferencesKey(key)] = value
is Float -> setting[floatPreferencesKey(key)] = value
is Boolean -> setting[booleanPreferencesKey(key)] = value
is String -> setting[stringPreferencesKey(key)] = value
is Set<*> -> {
val componentType = value::class.java.componentType!!
@Suppress("UNCHECKED_CAST") // Checked by reflection.
when {
String::class.java.isAssignableFrom(componentType) -> {
app.dataStore.edit { preferences ->
preferences[stringSetPreferencesKey(key)] = value as Set<String>
}
}
}
}
else -> {
throw IllegalArgumentException("This type can be saved into DataStore")
}
}
}
}
/**
* 获取数据
* */
suspend inline fun < reified T : Any> read(key: String, defaultValue:T): T {
return when (T::class) {
Int::class -> {
app.dataStore.data.map { setting ->
setting[intPreferencesKey(key)] ?: defaultValue
}.first() as T
}
Long::class -> {
app.dataStore.data.map { setting ->
setting[longPreferencesKey(key)] ?: defaultValue
}.first() as T
}
Double::class -> {
app.dataStore.data.map { setting ->
setting[doublePreferencesKey(key)] ?:defaultValue
}.first() as T
}
Float::class -> {
app.dataStore.data.map { setting ->
setting[floatPreferencesKey(key)] ?:defaultValue
}.first() as T
}
Boolean::class -> {
app.dataStore.data.map { setting ->
setting[booleanPreferencesKey(key)]?:defaultValue
}.first() as T
}
String::class -> {
app.dataStore.data.map { setting ->
setting[stringPreferencesKey(key)] ?: defaultValue
}.first() as T
}
else -> {
throw IllegalArgumentException("This type can be get into DataStore")
}
}
}
}