MMKV

MMKV 使用

简单使用

SP 迁移 MMKV 步骤

所有要迁移的 sp filename,可以用个数组列举出要迁移的 sp filename

// 需要迁移MMKV的sp filename
private val xmlIds = arrayOf(
    "buyers_guide",
    "SP_AROUTER_CACHE",
)

不能迁移到 mmkv 的 sp,如使用到 mmkv 未实现的 getAll 方法,可以弄个 blacklist

  1. WebViewProfilePrefsDefault
  2. WebViewChromiumPrefs
// 不能使用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

  1. Application
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
        )
    }
}
  1. Activity
class Activity {
	@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        return MMKVUtils.replaceSharedPreferences(getApplicationContext(), super.getSharedPreferences(name, mode), name);
    }
}

迁移失败后返回 NoMainThreadWriteSharedPreferences

NoMainThreadWriteSharedPreferences

NoMainThreadWriteSharedPreferences 是用来避免 ANR 的 SharedPreferences,子线程更新数据

/**
 *
 * 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

完整工具类

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))
            }
        }
    }

}
/**
 *
 * 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

  1. 注意 NPE 的问题

MMKV 不支持的 API

registerOnSharedPreferenceChangeListener/unregisterOnSharedPreferenceChangeListener 不支持

这 2 个 API 在 mmkv 中,会抛出异常?
mmkv 未设计数据变化监听,推荐开发者用 eventbus 这样的框架来实现;如果这样设计很糟糕

MMKV 不支持 getAll

mmkv 存储的时候,类型是擦除的

解决 1:用 allKeys() 替代 getAll()

存在的问题:mmkv 存储的时候进行的类型擦除,取出来的值不知道是什么类型

解决 2:寻找规律,自己判断类型

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
    }
}

解决 3:sp 升级到 mmkv 时,在 key 上带类型存储

具体的实现可参考这个:在初次使用mmvk时就做好包装,所以避免了后期无法迁移。

解决 4:带类型存储,flipper 支持 getAll,自动更新

  1. 存储时,key 带类型存储 (key@类型);需要封装 mmkv
  2. 封装支持自动更新

完善:https://github.com/hacket/mmkv-flipper
参考:https://github.com/porum/KVCompat

MMKV v1.2.15 类型擦除,不同类型直接 decode 测试

String

n0xta

k93os

Set<String>

b3o6k

781ws

bool

naiqn

int

rwdkm

fqus6

long

2fvg9

5bmik

float

9na7k

2neuz

double

7b19o

dernw

旧版本解决:

测试的版本 1.2.15,如果是 bool,decodeDouble/decodeFloat 返回 0.0 了,不再是一个 1.4E-451.4E-45

Ref

On this page
  • MMKV 使用
    1. 简单使用
    2. SP 迁移 MMKV 步骤
      1. 所有要迁移的 sp filename,可以用个数组列举出要迁移的 sp filename
      2. 不能迁移到 mmkv 的 sp,如使用到 mmkv 未实现的 getAll 方法,可以弄个 blacklist
      3. 只需要迁移一次,需要保存迁移的标记
      4. 重写 Application 和 Activity 的 getSharedPreferences,用 mmkv
      5. 迁移失败后返回 NoMainThreadWriteSharedPreferences
        1. NoMainThreadWriteSharedPreferences
      6. sp 迁移到 mmkv 时,mmkv 存储时类型是擦除的,所以最好是带上类型存储,否则 getAll 出来的数据不知道是什么类型
      7. 完整工具类
  • MMKV 存在的问题
  • 替换系统 SP
  • MMKV 不支持的 API
    1. registerOnSharedPreferenceChangeListener/unregisterOnSharedPreferenceChangeListener 不支持
    2. MMKV 不支持 getAll
      1. 解决 1:用 allKeys() 替代 getAll()
      2. 解决 2:寻找规律,自己判断类型
      3. 解决 3:sp 升级到 mmkv 时,在 key 上带类型存储
      4. 解决 4:带类型存储,flipper 支持 getAll,自动更新
  • MMKV v1.2.15 类型擦除,不同类型直接 decode 测试
  • String
  • Set
  • bool
  • int
  • long
  • float
  • double
  • Ref