MMKV 是腾讯的一套成熟的方案,他为了优化sp而生。
他的原理是采用修改内存的键值
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>