[实践—卡顿优化] 替换 SharePreferences 为 MMKV

替换 SharePreferences 为 MMKV

本文基于真实项目优化经验

众所周知 SharedPreferences 有性能问题,在压力大的情况下无论是读还是写都有 ARN 的可能,最近监控返现 SharedPreferences 的问题也出现在了 togo(我司项目代号) 中,还挺严重,决心搞掉他。

在这里插入图片描述
在这里插入图片描述

解决方案很明确:使用 MMKV 替换掉 SharePreferences。MMKV 项目地址:https://github.com/Tencent/MMKV

SharePreferences VS. MMKV

简单做一个对比吧

SharePreferencesMMKV
是否阻塞线程
是否线程安全
读取方式完整的 I/O 操作,需要经过用户态、内核态、文件系统通过 mmap 写入虚拟内存,0 拷贝和操作内存一样快
数据格式:xmlProtobuf
写入方式:全量更新增量更新
防数据丢失否,全量更新会丢数据是(基于 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 每次冷启动的时候都会造成数据丢失

资料

SharedPreferences替换:MMKV集成与原理

MacOs 下 MMKV 数据读取工具

让 MMKV 支持 getAll


版权声明:本文为qq_23191031原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。