替换 SharePreferences 为 MMKV
本文基于真实项目优化经验
众所周知 SharedPreferences 有性能问题,在压力大的情况下无论是读还是写都有 ARN 的可能,最近监控返现 SharedPreferences 的问题也出现在了 togo(我司项目代号) 中,还挺严重,决心搞掉他。
![]() |
---|
![]() |
解决方案很明确:使用 MMKV
替换掉 SharePreferences
。MMKV 项目地址:https://github.com/Tencent/MMKV
SharePreferences VS. MMKV
简单做一个对比吧
SharePreferences | MMKV | |
---|---|---|
是否阻塞线程 | 是 | 否 |
是否线程安全 | 是 | 是 |
读取方式 | 完整的 I/O 操作,需要经过用户态、内核态、文件系统 | 通过 mmap 写入虚拟内存,0 拷贝和操作内存一样快 |
数据格式: | xml | Protobuf |
写入方式: | 全量更新 | 增量更新 |
防数据丢失 | 否,全量更新会丢数据 | 是(基于 mmap 操作系统自动会写,不会应crash丢失数据) |
支持跨进程 | 否 | 是(基于 mmap + 进程锁) |
MMKV 核心是基于 mmap
实现的,具体原理可以看看我之前写过的一个文章:图解 mmap 原理。
更多二者对比内容可以看看这篇文章 SharedPreferences替换:MMKV集成与原理,里面对比的挺详细。
总的来说 MMKV 有如下优点:
1. 防止数据丢失,和操作内存一样快读写的效率;
2. 精简数据,以最少的数据量表示最多的信息,减少数据大小;
3. 增量更新,避免每次进行相对增量来说大数据量的全量写入。
方案1、覆写 Application.getSharePreferences
既然所有的 SharePreferences
都是通过 Application 获取的,那么直接在 Application 覆写 getSharePreferences 方法是不是就可以了呢。
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
MMKV mmkv = MMKV.mmkvWithID(name, mode);
// 不要反复操作 SharedPreferences
if (MMKV.defaultMMKV().getBoolean("mmkv_" + name, true)) {
SharedPreferences preferences = super.getSharedPreferences(name, mode);
mmkv.importFromSharedPreferences(preferences);
preferences.edit().clear().apply();
MMKV.defaultMMKV().putBoolean("mmkv_" + name, false);
}
return mmkv;
}
理想很丰满,现实很骨感,答案是不行! 原因是 MMKV 不支持 getAll()
public Map<String, ?> getAll() {
throw new UnsupportedOperationException("use allKeys() instead, getAll() not implement because type-erasure inside mmkv");
}
MMKV
都是按字节进行存储的,实际写入文件把类型擦除了,这也是MMKV
不支持getAll
的原因。
一旦覆写了 Application.getSharePreferences 方法,整个 app 的 SharePreferences 都被我们统一替换了,如果第三库或者系统这些代码我们无法修改的地方使用了 getAll 方法就会抛出如上问题。这显然是不可以接受的。
方案2、封装代理层,只处理自己的业务逻辑
统一全局替换不行,那么我们就只替换业务层逻辑。庆幸的是 togo 项目所有的 SharePreferences 都是统一使用了 PreferenceConfig 这个二次封装,只需要简单修改,向外暴露可注入 SharePreferences 实现的接口即可。
/**
* 基于 SharedPreferences 的存储类
*
* Created by mrsimple on 21/4/2018.
*/
public class PreferenceConfig {
protected Context mContext ;
protected String mPrefFileName;
protected static SharedPreferencesFactory sPreferencesFactory;
public PreferenceConfig(String prefFileName) {
this(AppContextHolder.getAppContext(), prefFileName);
}
public PreferenceConfig(Context context, String fileName) {
mContext = context;
this.mPrefFileName = !TextUtils.isEmpty(fileName) ? fileName : this.getClass().getSimpleName();
}
// 提供注入点
public static void setSharedPreferencesFactory(SharedPreferencesFactory factory) {
if (factory != null) {
sPreferencesFactory = factory;
}
}
public void saveString(String key, String value) {
if (!TextUtils.isEmpty(key)) {
getConfigPreferences().edit().putString(key, value).apply();
}
}
…………
protected SharedPreferences getConfigPreferences() {
if (sPreferencesFactory == null) {
sPreferencesFactory = new DefaultImpl();
}
return sPreferencesFactory.getSharedPreferences(mContext, mPrefFileName, Context.MODE_PRIVATE);
}
…………
/**
* SharedPreferences 工厂,外部可自定义 Preferences 的实现
*/
public interface SharedPreferencesFactory {
SharedPreferences getSharedPreferences(Context context, String name, int mode);
}
/**
* 默认实现
*/
public static class DefaultImpl implements SharedPreferencesFactory {
@Override
public SharedPreferences getSharedPreferences(final Context context, final String name, final int mode) {
return context.getSharedPreferences(name, Context.MODE_PRIVATE);
}
}
}
2.1、注入实现
private static void initMMKV(Application appContext) {
// 不能放在异步线程初始化,会导致内容失效
MMKV.initialize(appContext);
PreferenceConfig.setSharedPreferencesFactory(new PreferenceConfig.DefaultImpl() {
@Override
public SharedPreferences getSharedPreferences(final Context context, final String name, final int mode) {
MMKV mmkv = MMKV.mmkvWithID(name, mode);
// 不要反复操作 SharedPreferences,
// 添加 mmkv_ 前缀防止发生 key 冲突
if (MMKV.defaultMMKV().getBoolean("mmkv_" + name, true)) {
SharedPreferences preferences = super.getSharedPreferences(context, name, mode);
mmkv.importFromSharedPreferences(preferences);
preferences.edit().clear().apply();
MMKV.defaultMMKV().putBoolean("mmkv_" + name, false);
}
return mmkv;
}
});
}
成功注入后,只要全局搜索所有 getAll
使用的地方进行兼容即可。就实际使用情况来讲 getAll
并不是一个常用的方法,整个项目 + 自己维护的公有库(加起来也得有百万代码量了)就找到一处使用的地方。
对于 MMK 不支持 getAll
的问题,网上有一些兼容的方案:例如这篇文案的 让 MMKV 支持 getAll 其方案自己实现 SharedPreferences, SharedPreferences.Editor
接口做一个代理层是在存储数据时,在 key 上添加具体数据类型,这样一来读取 数据的时候就能通过 key 定位到数据类型,然后分类转换即可。是个可取的方案。
override fun getAll(): MutableMap<String, *> {
val keys = mmkv?.allKeys()
val map = mutableMapOf<String, Any>()
keys?.forEach {
if (it.contains("@")) {
val typeList = it.split("@")
when (typeList[typeList.size - 1]) {
String::class.simpleName -> map[it] = getString(it, "") ?: ""
Int::class.simpleName -> map[it] = getInt(it, 0)
Long::class.simpleName -> map[it] = getLong(it, 0L)
Float::class.simpleName -> map[it] = getFloat(it, 0f)
Boolean::class.simpleName -> map[it] = getBoolean(it, false)
}
}
}
return map
}
2.2、不要异步初始化
一开始将 initMMKV
方法的初始化工作放在了异步线程,这样做的目的是不想影响 Application#onCreate 的创建速度(最终是不影响 app 的启动速度)。但是经过几次试验发现,这个初始化过程必须在主线程完成,否则 App 每次冷启动的时候都会造成数据丢失