MMKV
MMKV
MMKV 使用
简单使用
SP 迁移 MMKV 步骤
所有要迁移的 sp filename,可以用个数组列举出要迁移的 sp filename
1
2
3
4
5
// 需要迁移MMKV的sp filename
private val xmlIds = arrayOf(
"buyers_guide",
"SP_AROUTER_CACHE",
)
不能迁移到 mmkv 的 sp,如使用到 mmkv 未实现的 getAll 方法,可以弄个 blacklist
- WebViewProfilePrefsDefault
- WebViewChromiumPrefs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 不能使用mmkv的SharedPreferences
private val blackList = arrayOf(
"WebViewProfilePrefsDefault", // 使用到mmkv未实现的getAll方法。
"WebViewChromiumPrefs",
"BraintreeApi", // BraintreeSharedPreferences 用到 androidx.security.crypto.EncryptedSharedPreferences
// 华为相关sp
"move_to_de_records",
"aaid",
"push_notify_flag",
"grs_move2DE_records",
"share_pre_grs_conf_",
"share_pre_grs_services_",
"hms_"
)
只需要迁移一次,需要保存迁移的标记
重写 Application 和 Activity 的 getSharedPreferences,用 mmkv
- Application
1
2
3
4
5
6
7
8
9
10
11
12
13
open class BaseApplication : Application() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MMKVUtils.init(base);
}
override fun getSharedPreferences(name: String, mode: Int): SharedPreferences? {
return MMKVUtils.replaceSharedPreferences(
applicationContext,
super.getSharedPreferences(name, mode),
name
)
}
}
- Activity
1
2
3
4
5
6
class Activity {
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return MMKVUtils.replaceSharedPreferences(getApplicationContext(), super.getSharedPreferences(name, mode), name);
}
}
迁移失败后返回 NoMainThreadWriteSharedPreferences
NoMainThreadWriteSharedPreferences
NoMainThreadWriteSharedPreferences 是用来避免 ANR 的 SharedPreferences,子线程更新数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/**
*
* https://gist.github.com/tprochazka/d91d89ec54bd6c3c1cb46f62faf3c12c
*
* ANR free implementation of SharedPreferences.
*
* Fast fix for ANR caused by writing all non written changes on main thread during activity/service start/stop.
*
* Disadvantage of current implementation:
* - OnSharedPreferenceChangeListener is called after all changes are written to disk.
* - If somebody will call edit() apply() several times after each other it will also several times write whole prefs file.
*
* Usage:
*
* Override this method in your Application class.
*
* public SharedPreferences getSharedPreferences(String name, int mode) {
* return NoMainThreadWriteSharedPreferences.getInstance(super.getSharedPreferences(name, mode), name);
* }
*
* You need to override also parent activity, because if somebody will use activity context instead
* of the application one, he will get a different implementation, you can do something like
*
* public SharedPreferences getSharedPreferences(String name, int mode) {
* return getApplicationContext().getSharedPreferences(name, mode);
* }
*
* @author Tomáš Procházka (prochazka)
*/
@RequiresApi(11)
class NoMainThreadWriteSharedPreferences private constructor(
private val sysPrefs: SharedPreferences,
val name: String
) :
SharedPreferences {
private val preferencesCache: MutableMap<String, Any?> = HashMap()
companion object {
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
private val INSTANCES: MutableMap<String, NoMainThreadWriteSharedPreferences> = HashMap()
@JvmStatic
fun getInstance(sharedPreferences: SharedPreferences, name: String): SharedPreferences {
return INSTANCES.getOrPut(
name,
{ NoMainThreadWriteSharedPreferences(sharedPreferences, name) })
}
/**
* Remove all instances for testing purpose.
*/
@VisibleForTesting
@JvmStatic
fun reset() {
INSTANCES.clear()
}
}
init {
/**
* I will think about it if there is no synchronization issue. But generally, I think that it will bring no difference. Because system shared preference itself loading whole properties file to memory anyway. So preferencesCache.putAll(sysPrefs.all) is just an in-memory operation that will be much faster than loading and parsing files from the storage.
*/
preferencesCache.putAll(sysPrefs.all)
}
override fun contains(key: String?) = preferencesCache[key] != null
override fun getAll() = HashMap(preferencesCache)
override fun getBoolean(key: String, defValue: Boolean): Boolean {
return preferencesCache[key] as Boolean? ?: defValue
}
override fun getInt(key: String, defValue: Int): Int {
return preferencesCache[key] as Int? ?: defValue
}
override fun getLong(key: String, defValue: Long): Long {
return preferencesCache[key] as Long? ?: defValue
}
override fun getFloat(key: String, defValue: Float): Float {
return preferencesCache[key] as Float? ?: defValue
}
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
@Suppress("UNCHECKED_CAST")
return preferencesCache[key] as MutableSet<String>? ?: defValues
}
override fun getString(key: String, defValue: String?): String? {
return preferencesCache[key] as String? ?: defValue
}
override fun edit(): SharedPreferences.Editor {
return Editor(sysPrefs.edit())
}
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
sysPrefs.registerOnSharedPreferenceChangeListener(listener)
}
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
sysPrefs.unregisterOnSharedPreferenceChangeListener(listener)
}
inner class Editor(private val sysEdit: SharedPreferences.Editor) : SharedPreferences.Editor {
private val modifiedData: MutableMap<String, Any?> = HashMap()
private var keysToRemove: MutableSet<String> = HashSet()
private var clear = false
override fun commit(): Boolean {
submit()
return true
}
override fun apply() {
submit()
}
private fun submit() {
synchronized(preferencesCache) {
storeMemCache()
queuePersistentStore()
}
}
private fun storeMemCache() {
if (clear) {
preferencesCache.clear()
clear = false
} else {
preferencesCache.keys.removeAll(keysToRemove)
}
keysToRemove.clear()
preferencesCache.putAll(modifiedData)
modifiedData.clear()
}
private fun queuePersistentStore() {
try {
executor.submit {
sysEdit.commit()
}
} catch (ex: Exception) {
Log.e(
"NoMainThreadWritePrefs",
"NoMainThreadWriteSharedPreferences.queuePersistentStore(), submit failed for $name"
)
}
}
override fun remove(key: String): SharedPreferences.Editor {
keysToRemove.add(key)
modifiedData.remove(key)
sysEdit.remove(key)
return this
}
override fun clear(): SharedPreferences.Editor {
clear = true
sysEdit.clear()
return this
}
override fun putLong(key: String, value: Long): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putLong(key, value)
return this
}
override fun putInt(key: String, value: Int): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putInt(key, value)
return this
}
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putBoolean(key, value)
return this
}
override fun putStringSet(
key: String,
values: MutableSet<String>?
): SharedPreferences.Editor {
modifiedData[key] = values
sysEdit.putStringSet(key, values)
return this
}
override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putFloat(key, value)
return this
}
override fun putString(key: String, value: String?): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putString(key, value)
return this
}
}
}
注意:替换掉系统的 SP,注意 null 的问题;我们有个项目用自己的 SP,用 ConcurrentHashMap 替换掉 HashMap,由于 ConcurrentHashMap 的 key 和 value 不能为 null,容易导致很隐蔽的一些 NPE 问题
sp 迁移到 mmkv 时,mmkv 存储时类型是擦除的,所以最好是带上类型存储,否则 getAll 出来的数据不知道是什么类型
在初次使用 mmvk 时就做好包装,带类型存储,所以避免了后期无法迁移。
具体见 MMKV存在的问题→MMKV不支持getAll
完整工具类
- MMKVUtils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
object MMKVUtils {
const val TAG = "mmkv"
private const val KEY_IMPORT = "has_old_sp_data_import"
private const val delOldSpData = false // 是否删除旧的SharedPreferences数据
private var hasImport = false
// 需要迁移MMKV的sp filename
private val xmlIds = arrayOf(
"buyers_guide",
"SP_AROUTER_CACHE",
)
// 不能使用mmkv的SharedPreferences
private val blackList = arrayOf(
"WebViewProfilePrefsDefault", // 使用到mmkv未实现的getAll方法。
"WebViewChromiumPrefs",
"BraintreeApi", // BraintreeSharedPreferences 用到 androidx.security.crypto.EncryptedSharedPreferences
// 华为相关sp
"move_to_de_records",
"aaid",
"push_notify_flag",
"grs_move2DE_records",
"share_pre_grs_conf_",
"share_pre_grs_services_",
"hms_"
)
/**
* 是否已经纳入mmkv管理
* @param name
* @return
*/
fun contains(context: Context, name: String): Boolean {
if (name == getDefaultId(context)) return true
for (item in xmlIds) {
if (item == name) {
return true
}
}
return false
}
fun isAvailable(name: String): Boolean {
for (item in blackList) {
if (name.contains(item)) {
return false
}
}
return true
}
/**
* 尽量提前初始化,最好是放到Application.attachBaseContext中,否则其他用了sp的地方会报错,因为在Activity和Application会替换掉系统的SharedPreferences导致出错
*/
@JvmStatic
fun init(context: Context) {
val logLevel =
if (BuildConfig.DEBUG) MMKVLogLevel.LevelDebug else MMKVLogLevel.LevelNone // 日志开关
val path: String = MMKV.initialize(
context,
{ libName -> ReLinker.loadLibrary(context, libName) },
logLevel
)
Log.d(TAG, "MMKV存放路径: $path")
// 迁移数据
Log.d(TAG, ">>>>>>MMKV开始迁移<<<<<<")
val start = System.currentTimeMillis()
hasImport = getBoolean(getDefaultId(context), KEY_IMPORT, false)
if (!hasImport) {
importDefaultSharedPreferences(context, getDefaultId(context))
importSharedPreferences(context)
putBoolean(getDefaultId(context), KEY_IMPORT, true)
}
val cost = System.currentTimeMillis() - start
Log.d(TAG, ">>>>>>MMKV迁移结束<<<<<< 总耗时:" + cost + "ms")
}
/**
* 遍历数组:迁移数据到mmkv
* @param context
*/
private fun importSharedPreferences(context: Context) {
try {
for (id in xmlIds) {
val prefs = context.getSharedPreferences(id, Context.MODE_PRIVATE)
if (prefs.all.isNotEmpty()) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
if (mmkv != null) {
mmkv.importFromSharedPreferences(prefs) // 迁移旧数据
if (delOldSpData) {
prefs.edit().clear().apply() // 清空旧数据
}
}
}
}
} catch (e: Throwable) {
Log.d(TAG, "数据迁移失败:" + e.message)
}
}
/**
* 迁移DefaultSharedPreferences数据,特殊处理
*
* @param context
* @param id
*/
private fun importDefaultSharedPreferences(context: Context, id: String) {
try {
val prefs = context.getSharedPreferences(id, Context.MODE_PRIVATE)
val prefsKeySize = prefs.all.size
if (prefsKeySize > 0) {
val originMmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
val mmkv = MMKVWrapper(originMmkv)
if (mmkv != null) {
// 判断是否已经迁移
if (mmkv.allKeys() != null) {
val mmkvKeySize = mmkv.allKeys()!!.size
if (mmkvKeySize > prefsKeySize && prefsKeySize < 10) {
return
}
}
// 迁移旧数据
mmkv.importFromSharedPreferences(prefs)
if (delOldSpData) {
// 清除旧数据
val editor = prefs.edit()
for (mutableEntry in prefs.all) {
val key = mutableEntry.key
val value = mutableEntry.value
if (key.startsWith("com.facebook.appevents.SessionInfo")
|| key.startsWith("IABUSPrivacy_String")
|| key.startsWith("variations_seed_native_stored")
) {
Log.d(
TAG,
"保留的三方key-value:key = $key,value = $value"
)
} else {
editor.remove(key)
}
}
editor.apply()
}
}
}
} catch (e: Throwable) {
Log.d(TAG, "数据迁移失败:" + e.message)
}
}
/**
* 设置字符串
*
* @param id
* @param key
* @param value
*/
@JvmStatic
fun putString(id: String, key: String, value: String?) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.encode(key, value)
}
/**
* 获取字符串
*
* @param id
* @param key
* @param defValue
* @return
*/
@JvmStatic
fun getString(id: String, key: String, defValue: String?): String? {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
return if (mmkv != null) mmkv.decodeString(key, defValue) else defValue
}
/**
* 保存Parcelable对象
*
* @param id
* @param key
* @param value
*/
fun putParcelable(id: String, key: String, value: Parcelable?) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.encode(key, value)
}
/**
* 获取Parcelable对象
*
* @param id
* @param key
* @return
*/
fun <T : Parcelable?> getParcelable(id: String, key: String, tClass: Class<T>): T? {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
return mmkv?.decodeParcelable(key, tClass)
}
/**
* 设置整型数值
*
* @param id
* @param key
* @param value
*/
fun putInt(id: String, key: String, value: Int) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.encode(key, value)
}
/**
* 获取整型数值
*
* @param id
* @param key
* @param defValue
* @return
*/
fun getInt(id: String, key: String, defValue: Int): Int {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
return mmkv?.decodeInt(key, defValue) ?: defValue
}
/**
* 设置bool值
*
* @param id
* @param key
* @param value
*/
@JvmStatic
fun putBoolean(id: String, key: String, value: Boolean) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.encode(key, value)
}
/**
* 获取bool值
*
* @param id
* @param key
* @param defValue
* @return
*/
@JvmStatic
fun getBoolean(id: String, key: String, defValue: Boolean): Boolean {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
return mmkv?.decodeBool(key, defValue) ?: defValue
}
/**
* 设置long值
*
* @param id
* @param key
* @param value
*/
fun putLong(id: String, key: String, value: Long) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.encode(key, value)
}
/**
* 获取long值
*
* @param id
* @param key
* @param defValue
* @return
*/
fun getLong(id: String, key: String, defValue: Long): Long {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
return mmkv?.decodeLong(key, defValue) ?: defValue
}
/**
* 设置float值
*
* @param id
* @param key
* @param value
*/
fun putFloat(id: String, key: String, value: Float) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.encode(key, value)
}
/**
* 获取float值
*
* @param id
* @param key
* @param defValue
* @return
*/
fun getFloat(id: String, key: String, defValue: Float): Float {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
return mmkv?.decodeFloat(key, defValue) ?: defValue
}
/**
* 设置double值
*
* @param id
* @param key
* @param value
*/
fun putDouble(id: String, key: String, value: Double) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.encode(key, value)
}
/**
* 获取double值
*
* @param id
* @param key
* @param defValue
* @return
*/
fun getDouble(id: String, key: String, defValue: Double): Double {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
return mmkv?.decodeDouble(key, defValue) ?: defValue
}
/**
* 设置字符串集合
*
* @param id
* @param key
* @param values
*/
fun putStringSet(id: String, key: String, values: Set<String?>?) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.putStringSet(key, values)
}
/**
* 获取字符串集合
*
* @param id
* @param key
* @param defValues
*/
fun getStringSet(id: String, key: String, defValues: Set<String?>?): Set<String?>? {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
return if (mmkv != null) mmkv.getStringSet(key, defValues) else defValues
}
/**
* 根据Key移除value
*
* @param id
* @param key
*/
fun remove(id: String, key: String) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.removeValueForKey(key)
}
/**
* 根据多个Key移除value
*
* @param id
* @param arrKeys
*/
fun removeKeys(id: String, arrKeys: Array<String?>) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.removeValuesForKeys(arrKeys)
}
/**
* 根据ID清除所有数据
*
* @param id
*/
fun clearAll(id: String) {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
mmkv?.clearAll()
}
/**
* 判断key是否存在
*
* @param id
* @param key
* @return
*/
fun contains(id: String, key: String): Boolean {
val mmkv = MMKV.mmkvWithID(id, MMKV.MULTI_PROCESS_MODE)
return mmkv != null && mmkv.contains(key)
}
private var defaultId: String = ""
/**
* 获取默认SharedPreferences_xml名称
*
* @return sp id
*/
@JvmStatic
fun getDefaultId(context: Context): String {
if (defaultId.isBlank()) {
defaultId = context.packageName + "_preferences"
}
return defaultId
}
/**
* 替换掉系统的SharedPreferences
*/
@JvmStatic
fun replaceSharedPreferences(
context: Context,
oldSP: SharedPreferences,
name: String
): SharedPreferences? {
// return NoMainThreadWriteSharedPreferences.getInstance(super.getSharedPreferences(name, mode), name);
return if (!hasImport && contains(context, name)
|| !isAvailable(name)
/** 迁移数据时需要返回默认的SP */
) {
NoMainThreadWriteSharedPreferences.getInstance(oldSP, name)
} else {
if (!getBoolean(getDefaultId(context), name, false)) {
try {
Log.d(TAG, ">>>>>>MMKV开始迁移<<<<<< : $name")
val start = System.currentTimeMillis()
if (oldSP.all.isNotEmpty()) {
val mmkv = MMKV.mmkvWithID(name, MMKV.MULTI_PROCESS_MODE)
if (mmkv != null) {
mmkv.importFromSharedPreferences(oldSP) // 迁移旧数据
if (delOldSpData) {
oldSP.edit().clear().apply() // 清空旧数据
}
}
}
val cost = System.currentTimeMillis() - start
Log.d(TAG, name + ": >>>>>>MMKV迁移结束<<<<<< 总耗时:" + cost + "ms")
putBoolean(getDefaultId(context), name, true)
MMKVWrapper(MMKV.mmkvWithID(name, MMKV.MULTI_PROCESS_MODE))
} catch (e: Exception) {
e.printStackTrace()
Log.d(
TAG,
"MMKV数据迁移失败,退回NoMainThreadWriteSharedPreferences:" + e.message
)
NoMainThreadWriteSharedPreferences.getInstance(oldSP, name)
}
} else {
Log.d(TAG, ">>>>>> MMKV已经迁移了,直接用MMKV <<<<<< : $name")
MMKVWrapper(MMKV.mmkvWithID(name, MMKV.MULTI_PROCESS_MODE))
}
}
}
}
- NoMainThreadWriteSharedPreferences
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/**
*
* https://gist.github.com/tprochazka/d91d89ec54bd6c3c1cb46f62faf3c12c
*
* ANR free implementation of SharedPreferences.
*
* Fast fix for ANR caused by writing all non written changes on main thread during activity/service start/stop.
*
* Disadvantage of current implementation:
* - OnSharedPreferenceChangeListener is called after all changes are written to disk.
* - If somebody will call edit() apply() several times after each other it will also several times write whole prefs file.
*
* Usage:
*
* Override this method in your Application class.
*
* public SharedPreferences getSharedPreferences(String name, int mode) {
* return NoMainThreadWriteSharedPreferences.getInstance(super.getSharedPreferences(name, mode), name);
* }
*
* You need to override also parent activity, because if somebody will use activity context instead
* of the application one, he will get a different implementation, you can do something like
*
* public SharedPreferences getSharedPreferences(String name, int mode) {
* return getApplicationContext().getSharedPreferences(name, mode);
* }
*
* @author Tomáš Procházka (prochazka)
*/
@RequiresApi(11)
class NoMainThreadWriteSharedPreferences private constructor(
private val sysPrefs: SharedPreferences,
val name: String
) :
SharedPreferences {
private val preferencesCache: MutableMap<String, Any?> = HashMap()
companion object {
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
private val INSTANCES: MutableMap<String, NoMainThreadWriteSharedPreferences> = HashMap()
@JvmStatic
fun getInstance(sharedPreferences: SharedPreferences, name: String): SharedPreferences {
return INSTANCES.getOrPut(
name,
{ NoMainThreadWriteSharedPreferences(sharedPreferences, name) })
}
/**
* Remove all instances for testing purpose.
*/
@VisibleForTesting
@JvmStatic
fun reset() {
INSTANCES.clear()
}
}
init {
/**
* I will think about it if there is no synchronization issue. But generally, I think that it will bring no difference. Because system shared preference itself loading whole properties file to memory anyway. So preferencesCache.putAll(sysPrefs.all) is just an in-memory operation that will be much faster than loading and parsing files from the storage.
*/
preferencesCache.putAll(sysPrefs.all)
}
override fun contains(key: String?) = preferencesCache[key] != null
override fun getAll() = HashMap(preferencesCache)
override fun getBoolean(key: String, defValue: Boolean): Boolean {
return preferencesCache[key] as Boolean? ?: defValue
}
override fun getInt(key: String, defValue: Int): Int {
return preferencesCache[key] as Int? ?: defValue
}
override fun getLong(key: String, defValue: Long): Long {
return preferencesCache[key] as Long? ?: defValue
}
override fun getFloat(key: String, defValue: Float): Float {
return preferencesCache[key] as Float? ?: defValue
}
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
@Suppress("UNCHECKED_CAST")
return preferencesCache[key] as MutableSet<String>? ?: defValues
}
override fun getString(key: String, defValue: String?): String? {
return preferencesCache[key] as String? ?: defValue
}
override fun edit(): SharedPreferences.Editor {
return Editor(sysPrefs.edit())
}
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
sysPrefs.registerOnSharedPreferenceChangeListener(listener)
}
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
sysPrefs.unregisterOnSharedPreferenceChangeListener(listener)
}
inner class Editor(private val sysEdit: SharedPreferences.Editor) : SharedPreferences.Editor {
private val modifiedData: MutableMap<String, Any?> = HashMap()
private var keysToRemove: MutableSet<String> = HashSet()
private var clear = false
override fun commit(): Boolean {
submit()
return true
}
override fun apply() {
submit()
}
private fun submit() {
synchronized(preferencesCache) {
storeMemCache()
queuePersistentStore()
}
}
private fun storeMemCache() {
if (clear) {
preferencesCache.clear()
clear = false
} else {
preferencesCache.keys.removeAll(keysToRemove)
}
keysToRemove.clear()
preferencesCache.putAll(modifiedData)
modifiedData.clear()
}
private fun queuePersistentStore() {
try {
executor.submit {
sysEdit.commit()
}
} catch (ex: Exception) {
Log.e(
"NoMainThreadWritePrefs",
"NoMainThreadWriteSharedPreferences.queuePersistentStore(), submit failed for $name"
)
}
}
override fun remove(key: String): SharedPreferences.Editor {
keysToRemove.add(key)
modifiedData.remove(key)
sysEdit.remove(key)
return this
}
override fun clear(): SharedPreferences.Editor {
clear = true
sysEdit.clear()
return this
}
override fun putLong(key: String, value: Long): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putLong(key, value)
return this
}
override fun putInt(key: String, value: Int): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putInt(key, value)
return this
}
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putBoolean(key, value)
return this
}
override fun putStringSet(
key: String,
values: MutableSet<String>?
): SharedPreferences.Editor {
modifiedData[key] = values
sysEdit.putStringSet(key, values)
return this
}
override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putFloat(key, value)
return this
}
override fun putString(key: String, value: String?): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putString(key, value)
return this
}
}
}
MMKV 存在的问题
替换系统 SP
- 注意 NPE 的问题
MMKV 不支持的 API
registerOnSharedPreferenceChangeListener/unregisterOnSharedPreferenceChangeListener 不支持
这 2 个 API 在 mmkv 中,会抛出异常?
mmkv 未设计数据变化监听,推荐开发者用 eventbus 这样的框架来实现;如果这样设计很糟糕
MMKV 不支持 getAll
mmkv 存储的时候,类型是擦除的
解决 1:用 allKeys() 替代 getAll()
- use allKeys() instead, getAll() not implement because type-erasure inside mmkv
- what to do with Compatibility of getAll() on Android? #168
存在的问题:mmkv 存储的时候进行的类型擦除,取出来的值不知道是什么类型
解决 2:寻找规律,自己判断类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
private fun getObjectValue2(mmkv: MMKV, key: String): Any? {
// 因为其他基础类型value会读成空字符串,所以不是空字符串即为string or string-set类型
val str = mmkv.decodeString(key)
if (!TextUtils.isEmpty(str)) {
// 判断 string or string-set
return if (str!![0].code == 5) { // mmkv v1.2.15 ENQ 对应ANSI控制字符,表示询问字符,用于数据通信中询问对方是否准备好,或者用于空操作
val strings = mmkv.decodeStringSet(key)
Log.d(
TAG,
"MMKVFlipperPlugin -> getObjectValue2 判定为Set<String>, key=$key, value=$strings"
)
strings
} else {
Log.d(
TAG,
"MMKVFlipperPlugin -> getObjectValue2 判定为String, key=$key, value=$str"
)
str
}
}
// float double类型可通过string-set配合判断:
// 通过数据分析可以看到类型为float或double时string类型为空字符串且string-set类型读出为null,此时float和double有值
// float有值时,decodeDouble为0.0;其他情况为double
val set = mmkv.decodeStringSet(key)
if (str == null && set == null) {
// float和double有值时,如果是float,decodeDouble会为0
val valueFloat = mmkv.decodeFloat(key)
val valueDouble = mmkv.decodeDouble(key)
return if (valueDouble == 0.0) { // 是float
Log.d(
TAG,
"MMKVFlipperPlugin -> getObjectValue2 判断为Float, key=$key, valueFloat=$valueDouble"
)
valueFloat
} else {
Log.d(
TAG,
"MMKVFlipperPlugin -> getObjectValue2 判断为Double, key=$key, valueDouble=$valueDouble"
)
valueDouble
}
}
// int long bool 类型的处理放在一起, int类型1和0等价于bool类型true和false
// 判断long或int类型时, 如果数据长度超出int的最大长度, 则long与int读出的数据不等, 可确定为long类型
val valueInt = mmkv.decodeInt(key)
val valueLong = mmkv.decodeLong(key)
// 如果int/long/bool都为0,说明没有值,全部0.0F存储,可能造成String类型在Flipper中编辑不了
if (valueInt == 0 && valueLong == 0L) {
Log.d(
TAG,
"MMKVFlipperPlugin -> getObjectValue2 判断为Float, 如果int/long/bool都为0,说明没有值,全部以0.0存储 key=$key, value=$valueInt"
)
return 0.0F
}
Log.v(
TAG,
"MMKVFlipperPlugin -> getObjectValue2 key=$key, valueInt=$valueInt, valueLong=$valueLong"
)
return if (valueInt.toLong() != valueLong) {
Log.d(
TAG,
"MMKVFlipperPlugin -> getObjectValue2 判断为long, key=$key, valueLong=$valueLong"
)
valueLong
} else {
Log.d(
TAG,
"MMKVFlipperPlugin -> getObjectValue2 判断为int, key=$key, valueInt=$valueInt"
)
valueInt
}
}
- 微信开源库MMKV遍历读取存储的所有key以及对应的value方法(旧版本的,新版本不适用)
解决 3:sp 升级到 mmkv 时,在 key 上带类型存储
具体的实现可参考这个:在初次使用mmvk时就做好包装,所以避免了后期无法迁移。
解决 4:带类型存储,flipper 支持 getAll,自动更新
- 存储时,key 带类型存储 (
key@类型
);需要封装 mmkv - 封装支持自动更新
完善:https://github.com/hacket/mmkv-flipper
参考:https://github.com/porum/KVCompat
MMKV v1.2.15 类型擦除,不同类型直接 decode 测试
String
- test_string=”” 空串
- test_string = “hacket”
Set<String>
- test_string_set = setOf()
test_string_set = setOf<String>("hello","world")
bool
- test_bool = true
int
- test_init = 0
- test_init = 110
long
- test_long = 0L
- test_long = 111110000L
float
- test_float = 0.0F
- test_float = 110.110f
double
- test_double = 0.0
- test_double = 1111100000.111000
旧版本解决:
测试的版本 1.2.15,如果是 bool,decodeDouble/decodeFloat 返回 0.0 了,不再是一个 1.4E-45
或 1.4E-45
Ref
本文由作者按照 CC BY 4.0 进行授权