MMKV 是腾讯的一套成熟的方案,他为了优化sp而生。

他的原理是采用修改内存的键值 微信自用高性能通用key-value组件MMKV已开源!

1、MMKV简介

腾讯微信团队于2018年9月底宣布开源 MMKV ,这是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,主打高性能和稳定性。近期也已移植到 Android 平台,一并对外开源。

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今,在 iOS 微信上使用已有近 3 年,其性能和稳定性经过了时间的验证。近期也已移植到 Android 平台,一并开源。

MMKV最新源码托管地址:https://github.com/Tencent/MMKV

2、MMKV 源起

在微信客户端的日常运营中,时不时就会爆发特殊文字引起系统的 crash(请参见文章:《微信团队分享:iOS版微信是如何防止特殊字符导致的炸群、APP崩溃的?》、《微信团队分享:iOS版微信的高性能通用key-value组件技术实践》),文章里面设计的技术方案是在关键代码前后进行计数器的加减,通过检查计数器的异常,来发现引起闪退的异常文字。在会话列表、会话界面等有大量 cell 的地方,希望新加的计时器不会影响滑动性能;另外这些计数器还要永久存储下来——因为闪退随时可能发生。

这就需要一个性能非常高的通用 key-value 存储组件,我们考察了 SharedPreferences、NSUserDefaults、SQLite 等常见组件,发现都没能满足如此苛刻的性能要求。考虑到这个防 crash 方案最主要的诉求还是实时写入,而 mmap 内存映射文件刚好满足这种需求,我们尝试通过它来实现一套 key-value 组件。

3、MMKV 原理

内存准备:

通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。

数据组织:

数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。

写入优化:

考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。

空间增长:

使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。

更详细的设计原理参考MMKV 原理

4、iOS 指南

安装引入(推荐使用 CocoaPods):

安装CocoaPods

打开命令行,cd到你的项目工程目录, 输入pod repo update让 CocoaPods 感知最新的 MMKV 版本;

打开 Podfile, 添加pod 'MMKV'到你的 app target 里面;

在命令行输入pod install;

用 Xcode 打开由 CocoaPods 自动生成的.xcworkspace文件;

添加头文件#import <MMKV/MMKV.h>,就可以愉快地开始你的 MMKV 之旅了。

更多安装指引参考iOS Setup

快速上手:

MMKV 的使用非常简单,无需任何配置,所有变更立马生效,无需调用synchronize:

MMKV *mmkv = [MMKV defaultMMKV];    [mmkvsetBool:YESforKey:@"bool"];BOOL bValue = [mmkvgetBoolForKey:@"bool"];    [mmkvsetInt32:-1024forKey:@"int32"];int32_t iValue = [mmkvgetInt32ForKey:@"int32"];    [mmkvsetObject:@"hello, mmkv"forKey:@"string"];NSString *str = [mmkvgetObjectOfClass:NSString.classforKey:@"string"];

更详细的使用教程参考iOS Tutorial

性能对比:

循环写入随机的int1w 次,我们有如下性能对比:

更详细的性能对比参考iOS Benchmark

5、Android 指南

安装引入:

推荐使用 Maven:

dependencies{implementation'com.tencent:mmkv:1.0.10'// replace"1.0.10"with any available version}

更多安装指引参考Android Setup

快速上手:

MMKV 的使用非常简单,所有变更立马生效,无需调用sync、apply。 在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),例如在 MainActivity 里:

protectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);    String rootDir = MMKV.initialize(this);    System.out.println("mmkv root: "+ rootDir);//……}

MMKV 提供一个全局的实例,可以直接使用:

importcom.tencent.mmkv.MMKV;//……MMKV kv = MMKV.defaultMMKV();kv.encode("bool",true);booleanbValue = kv.decodeBool("bool");kv.encode("int", Integer.MIN_VALUE);intiValue = kv.decodeInt("int");kv.encode("string","Hello from mmkv");String str = kv.decodeString("string");

MMKV 支持多进程访问,更详细的用法参考Android Tutorial

性能对比:

循环写入随机的int1k 次,我们有如下性能对比:

更详细的性能对比参考Android Benchmark

下面附上一个android的使用demo 自己动手 丰衣足食。。

MmkvActivity

package com.liuan.ok_demo;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.tencent.mmkv.MMKV;

import java.util.Arrays;

import androidx.appcompat.app.AppCompatActivity;

public class MmkvActivity extends AppCompatActivity implements View.OnClickListener {


    private MMKV kv;
    /**
     * 把sp移动到mmkv
     */
    private Button mBtSpmove;
    /**
     * sp_保存
     */
    private Button mBtSaveSp;
    /**
     * sp_读取
     */
    private Button mBtReadSp;
    /**  */
    private TextView mTvShow;
    /**
     * mmkv保存通用
     */
    private Button mBtSaveMmkvNormal;
    /**
     * mmkv读取通用
     */
    private Button mBtReadMmkvNormal;
    /**
     * mmkv保存_withId
     */
    private Button mBtSaveMmkvWithid;
    /**
     * mmkv读取_withId
     */
    private Button mBtReadMmkvWithid;
    private MMKV kvWithId;
    /**
     * mmkv删除通用
     */
    private Button mBtDeleteMmkvnomarl;
    /**
     * mmkv删除_withId
     */
    private Button mBtDeleteMmkvWithid;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mmkv);
        kv = MMKV.defaultMMKV();
        kvWithId = MMKV.mmkvWithID("myData");
        initView();
    }

    private void debug(String text) {
        mTvShow.setText(text);
    }

    private void initView() {

        mBtSpmove = (Button) findViewById(R.id.bt_spmove);
        mBtSpmove.setOnClickListener(this);
        mBtSaveSp = (Button) findViewById(R.id.bt_save_sp);
        mBtSaveSp.setOnClickListener(this);
        mBtReadSp = (Button) findViewById(R.id.bt_read_sp);
        mBtReadSp.setOnClickListener(this);
        mTvShow = (TextView) findViewById(R.id.tv_show);
        mBtSaveMmkvNormal = (Button) findViewById(R.id.bt_save_mmkv_normal);
        mBtSaveMmkvNormal.setOnClickListener(this);
        mBtReadMmkvNormal = (Button) findViewById(R.id.bt_read_mmkv_normal);
        mBtReadMmkvNormal.setOnClickListener(this);
        mBtSaveMmkvWithid = (Button) findViewById(R.id.bt_save_mmkv_withid);
        mBtSaveMmkvWithid.setOnClickListener(this);
        mBtReadMmkvWithid = (Button) findViewById(R.id.bt_read_mmkv_withid);
        mBtReadMmkvWithid.setOnClickListener(this);
        mTvShow.setOnClickListener(this);
        mBtDeleteMmkvnomarl = (Button) findViewById(R.id.bt_delete_mmkvnomarl);
        mBtDeleteMmkvnomarl.setOnClickListener(this);
        mBtDeleteMmkvWithid = (Button) findViewById(R.id.bt_delete_mmkv_withid);
        mBtDeleteMmkvWithid.setOnClickListener(this);
    }

    private static final String TAG = "MmkvActivity";

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            default:
                break;
            case R.id.bt_save_mmkv_normal:
                kv.encode("bool", true);
                kv.encode("int", Integer.MIN_VALUE);
                kv.encode("string", "Hello from mmkv");
                break;
            case R.id.bt_read_mmkv_normal:
                boolean bValue = kv.decodeBool("bool");
                String str = kv.decodeString("string");
                int iValue = kv.decodeInt("int");
                String str1 = "bValue:" + bValue + "\n";
                str1 += "str:" + str + "\n";
                str1 += "iValue:" + iValue + "\n";
                debug(str1);
                break;
            case R.id.bt_spmove:
                testImportSharedPreferences();
                break;
            case R.id.bt_save_sp:
                SpUtils.putBoolean("sp_bool", true);
                SpUtils.putString("sp_string", "str");
                debug("存储成功");
                break;
            case R.id.bt_read_sp:
                boolean sp_bool = SpUtils.getBoolean("sp_bool", false);
                String sp_str = SpUtils.getString("sp_string", "");
                String str2 = "sp_bool:" + sp_bool + "\n";
                str2 += "sp_string:" + sp_str + "\n";
                debug(str2);
                break;
//                单独的id 和之前通用的id 不会混淆掉
            case R.id.bt_save_mmkv_withid:
                kvWithId.encode("string", "kvWithId");
                break;
            case R.id.bt_read_mmkv_withid:
                String string = kvWithId.decodeString("string");
                debug(string);
                break;
            case R.id.tv_show:
                mTvShow.setText("点我没有用。点按钮");
                break;
            case R.id.bt_delete_mmkvnomarl:
                removeForKv(kv);
                break;
            case R.id.bt_delete_mmkv_withid:
                removeForKv(kvWithId);
                break;
        }
    }

    private void removeForKv(MMKV kv) {
        kv.encode("string", "今天中午吃什么");
        kv.removeValueForKey("string");
        String string1 = "string: " + kv.decodeString("string") + "\n";

        kv.encode("int", Integer.MIN_VALUE);
        String string2 = "int: " + kv.decodeString("int") + "\n";
        kv.encode("long", Long.MAX_VALUE);

        kv.removeValuesForKeys(new String[]{"int", "long"});
        String string3 = "long: " + kv.decodeLong("long") + "\n";

        boolean hasBool = kv.containsKey("bool");
        String string4 = "hasBool: " + hasBool + "\n";
        debug(string1 + string2 + string3 + string4);

    }


    private void testImportSharedPreferences() {

        {
            SharedPreferences old_man = getSharedPreferences("config", MODE_PRIVATE);
            kv.importFromSharedPreferences(old_man);
            old_man.edit().clear().commit();
        }
        boolean bValue = kv.decodeBool("sp_bool");
        String str = kv.decodeString("sp_string");
        String str1 = "bValue:" + bValue + "\n";
        str1 += "str:" + str + "\n";
        debug("sp已经清除并转移到mmkv\n" + str1);
    }
}

SpUtils

package com.liuan.ok_demo;

import android.content.Context;
import android.content.SharedPreferences;


public class SpUtils {

    private static SharedPreferences sp;

    public static void getSharedPreference(Context context) {
        if (sp == null) {
            sp = context.getSharedPreferences("config", context.MODE_PRIVATE);
        }
    }

    public static void putString(Context context, String key, String value) {
        getSharedPreference(context);
        sp.edit().putString(key, value).commit();
    }

    private  static Context appContext;

    public static void init(Context context) {
        appContext = context;
    }

    public static String getString(Context context, String key, String defValue) {
        getSharedPreference(context);
        return sp.getString(key, defValue);
    }

    public static void putInt(Context context, String key, int value) {
        getSharedPreference(context);
        sp.edit().putInt(key, value).commit();
    }

    public static int getInt(Context context, String key, int defValue) {
        getSharedPreference(context);
        return sp.getInt(key, defValue);
    }

    public static void putBoolean(Context context, String key, boolean value) {
        getSharedPreference(context);
        sp.edit().putBoolean(key, value).commit();
    }

    public static boolean getBoolean(Context context, String key,
                                     boolean defValue) {
        getSharedPreference(context);
        return sp.getBoolean(key, defValue);
    }

    public static void putLong(Context context, String key, Long value) {
        getSharedPreference(context);
        sp.edit().putLong(key, value).commit();
    }

    public static Long getLong(Context context, String key,
                               Long defValue) {
        getSharedPreference(context);
        return sp.getLong(key, defValue);
    }


    /**
     * 移除
     */
    public static void remove(Context context, String key) {
        getSharedPreference(context);
        sp.edit().remove(key).commit();
    }

    //单参数构造
    public static void putString(String key, String value) {
        getSharedPreference(appContext);
        sp.edit().putString(key, value).commit();
    }

    public static String getString(String key, String defValue) {
        getSharedPreference(appContext);
        return sp.getString(key, defValue);
    }

    public static void putInt(String key, int value) {
        getSharedPreference(appContext);
        sp.edit().putInt(key, value).commit();
    }

    public static int getInt(String key, int defValue) {
        getSharedPreference(appContext);
        return sp.getInt(key, defValue);
    }

    public static void putBoolean(String key, boolean value) {
        getSharedPreference(appContext);
        sp.edit().putBoolean(key, value).commit();
    }

    public static boolean getBoolean(String key,
                                     boolean defValue) {
        getSharedPreference(appContext);
        return sp.getBoolean(key, defValue);
    }

    public static void putLong(String key, Long value) {
        getSharedPreference(appContext);
        sp.edit().putLong(key, value).commit();
    }

    public static Long getLong(String key,
                               Long defValue) {
        getSharedPreference(appContext);
        return sp.getLong(key, defValue);
    }


    /**
     * 移除
     */
    public static void remove(String key) {
        getSharedPreference(appContext);
        sp.edit().remove(key).commit();
    }


}  

MainActivity

package com.liuan.ok_demo;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.tencent.mmkv.MMKV;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    /**
     * Test MMkv
     */
    private Button mBtMmkv;
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        String rootDir = MMKV.initialize(this);
        SpUtils.init(this);
        Log.e(TAG, "onCreate: " + "mmkv root: " + rootDir);

    }



    private void initView() {
        mBtMmkv = (Button) findViewById(R.id.bt_mmkv);
        mBtMmkv.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            default:
                break;
            case R.id.bt_mmkv:
                startActivity(new Intent(this, MmkvActivity.class));

                break;
        }
    }
}

activity_mmkv.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/bt_save_mmkv_normal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="mmkv保存通用" />

    <Button
        android:id="@+id/bt_read_mmkv_normal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="mmkv读取通用" />
    <Button
        android:id="@+id/bt_delete_mmkvnomarl"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="mmkv删除通用" />

    <Button
        android:id="@+id/bt_save_mmkv_withid"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="mmkv保存_withId" />

    <Button
        android:id="@+id/bt_read_mmkv_withid"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="mmkv读取_withId" />
    <Button
        android:id="@+id/bt_delete_mmkv_withid"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="mmkv删除_withId" />



    <Button
        android:id="@+id/bt_save_sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="sp_保存" />

    <Button
        android:id="@+id/bt_read_sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="sp_读取" />

    <Button
        android:id="@+id/bt_spmove"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="把sp移动到mmkv" />

    <TextView
        android:id="@+id/tv_show"
        android:text=""
        android:textSize="18sp"
        android:textColor="@color/colorAccent"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/bt_mmkv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Test MMkv"
   />

</LinearLayout>

 

相关文章: