目录
1. MMKV——基于 mmap 的高性能通用 key-value 组件
一. 背景
任何一个应用程序都在不停地和数据打交道,我们使用程序时所关心的都是里面的数据。为了保证一些关键性的数据不会丢失,需要用到数据持久化技术。
二. 持久化技术简介
1. 瞬时数据:存储在内存当中,有可能会因为程序关闭或其他原因导致内存被回收而丢失的数据。
2. 数据持久化:将内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失。
3. 数据状态转换:保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换。
4. Android系统提供的3种持久化技术:文件存储、SharedPreferences存储以及数据库存储。
三. 文件存储
(1)简介
文件存储。Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合存储一些简单的文本数据或二进制数据。
(2)将数据存储到文件中
1. openFileOutput()方法
Context
类中提供了一个openFileOutput()
方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数:第一个参数是文件名,在文件创建的时候使用。第二个参数是文件的操作模式。
val output = openFileOutput("data", Context.MODE_PRIVATE)
- 指定的文件名不可以包含路径,所有的文件都默认存储到
- /data/data/<package name>/files/目录下;
- 操作模式可选
MODE_PRIVATE
和MODE_APPEND,
默认值为MODE_PRIVATE
,表示当指定相同文件名的时候,所写入的内容将会覆盖原文件中的内容;MODE_APPEND
则表示如果该文件已存在,就往文件里面追加内容,不存在就创建新文件。
2. 保存方法示例
以下是一段简单的代码示例,展示了如何将一段文本内容保存到文件中:
fun save(inputText: String) {
try {
// 1.通过openFileOutput()方法获取一个FileOutputStream对象
val output = openFileOutput("data", Context.MODE_PRIVATE)
// 2.借助FileOutputStream对象,构建出一个OutputStreamWriter对象。然后依靠OutputStreamWriter对象,构建一个BufferedWriter对象,通过其将文本内容写入文件中
val writer = BufferedWriter(OutputStreamWriter(output))
// 3.内置扩展函数use,在Lambda表达式中的代码全部执行完之后自动将外层的流关闭,无需再编写一个finally语句,手动去关闭流
writer.use {
it.write(inputText)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
(3)从文件中读取数据
1. openFileInput()方法
Context
类中还提供了一个openFileInput()
方法,用于从文件中读取数据。这个方法只接收一个参数,即要读取的文件名,然后系统会自动到/data/data/<package name>/files/目录下加载这个文件,并返回一个FileInputStream
对象,得到这个对象之后,再通过流的方式就可以将数据读取出来了。
2. 读取方法示例
以下是一段简单的代码示例,展示了如何从文件中读取文本数据:
fun load(): String {
val content = StringBuilder()
try {
// 注释1
val input = openFileInput("data")
// 注释2
val reader = BufferedReader(InputStreamReader(input))
reader.use {
// 注释3
reader.forEachLine {
// 注释4
content.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
// 注释5
return content.toString()
}
注释1:获取一个FileInputStream对象
注释2:借助FileInputStream对象构建出一个InputStreamReader对象,依靠InputStreamReader对象,构建一个BufferedReader对象,通过其将文件中的数据一行行读取出来
注释3:内置扩展函数forEachLine,它会将读到的每行内容都回调到Lambda表达式中,我们在Lambda表达式中完成拼接逻辑即可。
注释4:拼接到StringBuilder对象当中。
注释5:最后将读取的内容返回就可以了
四. SharedPreferences存储
(1)简介
不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的。也就是说,当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。
而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串。
(2)往SharedPreferences中存储数据
1.获取SharedPreferences
对象
Context
类中的getSharedPreferences()
方法,此方法接收两个参数。
- 第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data/<package name>/shared_prefs/目录下的;
- 第二个参数用于指定操作模式,目前只有默认的
MODE_PRIVATE
这一种模式可选,它和直接传入0的效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
Activity
类中的getPreferences()
方法,该方法只接收一个参数用于指定操作模式。使用这个方法时会自动将当前Activity
的类名作为SharedPreferences的文件名。
2. 向SharedPreferences文件中存储数据
得到了
SharedPreferences
对象之后,就可以开始存储数据了,主要可以分为3步实现。(1) 调用
SharedPreferences
对象的edit()
方法获取一个SharedPreferences.Editor
对象。(2) 向
SharedPreferences.Editor
对象中添加数据,比如添加一个布尔型数据就使用putBoolean()
方法,添加一个字符串则使用putString()
方法,以此类推。(3) 调用
apply()
方法将添加的数据提交,从而完成数据存储操作。
3. 项目示例
新建一个SharedPreferencesTest项目,然后修改activity_main.xml中的代码,简单地放置了一个按钮,用于将一些数据存储到SharedPreferences文件当中,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/saveButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Data"
/>
</LinearLayout>
然后修改MainActivity中的代码,如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
saveButton.setOnClickListener {
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()
}
}
}
可以看到,这里首先给按钮注册了一个点击事件,然后在点击事件中通过
getSharedPreferences()
方法指定SharedPreferences的文件名为data,并得到了SharedPreferences.Editor
对象。接着向这个对象中添加了3条不同类型的数据,最后调用apply()
方法进行提交,从而完成了数据存储的操作 .
(3)从SharedPreferences中读取数据
SharedPreferences
对象中提供了一系列的get
方法,用于读取存储的数据,每种get
方法都对应了SharedPreferences.Editor
中的一种put
方法,比如读取一个布尔型数据就使用getBoolean()
方法,读取一个字符串就使用getString()
方法。这些
get
方法都接收两个参数:第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;
第二个参数是默认值,当传入的键找不到对应的值时会以默认值进行返回。
在SharedPreferencesTest项目的基础上继续开发,修改activity_main.xml中的代码,增加了一个还原数据的按钮,我们希望通过点击这个按钮来从SharedPreferences文件中读取数据,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/saveButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Data"
/>
<Button
android:id="@+id/restoreButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restore Data"
/>
</LinearLayout>
修改MainActivity中的代码,在还原数据按钮的点击事件中首先通过getSharedPreferences()
方法得到了SharedPreferences
对象,然后分别调用它的getString()
、getInt()
和getBoolean()
方法,去获取前面所存储的姓名、年龄和是否已婚,如果没有找到相应的值,就会使用方法中传入的默认值来代替,最后通过Log将这些值打印出来。如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
restoreButton.setOnClickListener {
val prefs = getSharedPreferences("data", Context.MODE_PRIVATE)
val name = prefs.getString("name", "")
val age = prefs.getInt("age", 0)
val married = prefs.getBoolean("married", false)
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "age is $age")
Log.d("MainActivity", "married is $married")
}
}
}
五. MMKV概述
1. MMKV——基于 mmap 的高性能通用 key-value 组件
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。
GitHub地址:https://github.com/Tencent/MMKV
2. MMKV 原理
(1)内存准备:通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
(2)数据组织:数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
(3)写入优化:考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
(4)空间增长:使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
六. MMKV在Android中的使用
1. 添加依赖
dependencies {
implementation 'com.tencent:mmkv-static:1.2.2''
}
2. 初始化
MMKV 的使用非常简单,所有变更立马生效,无需调用
sync
、apply
。在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),例如
public class MyApp extends Application {
private static final String TAG = MyApp.class.getSimpleName();
@Override
public void onCreate() {
super.onCreate();
String rootDir = MMKV.initialize(this);
Log.i(TAG,"mmkv root: " + rootDir);
}
}
3. 支持的数据类型
- 支持以下Java语言基础类型:
boolean、int、long、float、double、byte[]。
支持以下Java类和容器:
String、Set<String>、任何实现了Parcelable的类型。
4. CRUD操作
(1)存储与读取数据
1.MMKV会提供一个全局实例,可以直接使用
2.存储数据采用键值对的方法进行存储,使用数据相应的encode()方法
3.读取数据采用decode()方法,传入键作为参数,读取Int类型就用decodeInt(),以此类推
import com.tencent.mmkv.MMKV;
...
MMKV kv = MMKV.defaultMMKV();
kv.encode("bool", true);
System.out.println("bool: " + kv.decodeBool("bool"));
kv.encode("int", Integer.MIN_VALUE);
System.out.println("int: " + kv.decodeInt("int"));
kv.encode("long", Long.MAX_VALUE);
System.out.println("long: " + kv.decodeLong("long"));
kv.encode("float", -3.14f);
System.out.println("float: " + kv.decodeFloat("float"));
kv.encode("double", Double.MIN_VALUE);
System.out.println("double: " + kv.decodeDouble("double"));
kv.encode("string", "Hello from mmkv");
System.out.println("string: " + kv.decodeString("string"));
byte[] bytes = {'m', 'm', 'k', 'v'};
kv.encode("bytes", bytes);
System.out.println("bytes: " + new String(kv.decodeBytes("bytes")));
(2)删除与查询
- removeValueForKey()方法移除指定的key
- removeValueForKeys()方法移除一组key
MMKV kv = MMKV.defaultMMKV();
// 移除指定的key
kv.removeValueForKey("bool");
System.out.println("bool: " + kv.decodeBool("bool"));
// 移除一组key
kv.removeValuesForKeys(new String[]{"int", "long"});
System.out.println("allKeys: " + Arrays.toString(kv.allKeys()));
boolean hasBool = kv.containsKey("bool");
(3)不同业务需要区别存储时
// 单独创建自己的实例
MMKV mmkv = MMKV.mmkvWithID("MyID");
mmkv.encode("bool", true);
(4)业务需要多进程访问时
// 在初始化的时候加上标志位 MMKV.MULTI_PROCESS_MODE
MMKV mmkv = MMKV.mmkvWithID("InterProcessKV", MMKV.MULTI_PROCESS_MODE);
mmkv.encode("bool", true);
5. SharedPreferences 迁移
- MMKV 提供了
importFromSharedPreferences()
函数,可以比较方便地迁移数据过来。- MMKV 还额外实现了一遍
SharedPreferences
、SharedPreferences.Editor
这两个 interface,在迁移的时候只需两三行代码即可,其他 CRUD 操作代码都不用改。
private void testImportSharedPreferences() {
//SharedPreferences preferences = getSharedPreferences("myData", MODE_PRIVATE);
MMKV preferences = MMKV.mmkvWithID("myData");
// 迁移旧数据
{
SharedPreferences old_man = getSharedPreferences("myData", MODE_PRIVATE);
preferences.importFromSharedPreferences(old_man);
old_man.edit().clear().commit();
}
// 跟以前用法一样
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean("bool", true);
editor.putInt("int", Integer.MIN_VALUE);
editor.putLong("long", Long.MAX_VALUE);
editor.putFloat("float", -3.14f);
editor.putString("string", "hello, imported");
HashSet<String> set = new HashSet<String>();
set.add("W"); set.add("e"); set.add("C"); set.add("h"); set.add("a"); set.add("t");
editor.putStringSet("string-set", set);
// 无需调用 commit()
//editor.commit();
}