Android MMKV框架引入使用

Android 敏捷开发助手

  1. Lottie动画 轻松使用
  2. PNG、JPG等普通图片高保真转SVG图
  3. Android 完美的蒙层方案
  4. Android MMKV框架引入使用
  5. 强大无匹的自定义下拉列表
  6. Google Protobuf 实践使用开发

MMKV框架引入使用

  • 前言
  • MMKV 原理
  • 功能特性
  • 使用实践
  • MMKV 、 SharedPreferences、SQLite 对比
  • SharedPreferences 迁移
  • 总结

博客创建时间:2022.10.04
博客更新时间:2023.01.28

以Android studio build=7.0.0,SDKVersion 31来分析讲解。如图文和网上其他资料不一致,可能是别的资料版本较低而已。


前言

项目常见的轻量级存储一般使用的是SharedPreferences,虽然 SP 兼容性极好, 但其低性能一直被诟病,线上也常出现一些SP导致的ANR。

鹅厂的开源框架MMKV能完美解决SP现有缺点并保持原有的优点。MMKV是基于 mmap 内存映射+ protobuf 序列化两者优势于一体的框架,其具有更高性能高,更强的稳定性。

框架源码:https://github.com/tencent/mmkv


MMKV 原理

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

数据组织
数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
Google Protobuf 实践使用开发

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

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

其中对于Android系统,增加了 文件锁 来保证多进程的调用。


功能特性

多进程访问
基于mmap技术,支持多进程数据共享

Android 平台第一个想到的就是 ContentProvider:一个单独进程管理数据,数据同步不易出错,简单好用易上手。然而它的问题也很明显,就是一个字慢:启动慢,访问也慢。这个可以说是 Android 下基于Binder的CS 架构组件的通用痛点。

再考虑到 MMKV 底层使用 mmap 实现,采用去中心化的架构是很自然的选择。我们只需要将文件 mmap 到每个访问进程的内存空间,加上合适的进程锁,再处理好数据的同步,就能够实现多进程并发访问。

匿名内存
对于敏感数据保存在文件中不适合,使用Android 特有的Ashmem 匿名共享内存技术,达到数据保密效果。

数据加密
在Android中 MMKV 使用了 AES CFB-128 算法来加密/解密。选择 CFB 而不是常见的 CBC 算法,主要是因为 MMKV 使用 append-only 实现插入/更新操作,流式加密算法更加合适。


使用实践

Android MMKV框架引入使用_第1张图片

MMKV 的使用非常简单,所有变更立马生效,无需调用 sync、apply。

1. 依赖

implementation 'com.tencent:mmkv:1.0.10'
implementation 'com.tencent:mmkv-static:1.0.23'

2. 初始化
MMKV的初始化可以指定保存位置,也可以默认保存位置。

// 初始化,默认的保存地址是: /data/user/0/应用包名/files/mmkv
String rootDir = MMKV.initialize(context);

// 指定保存位置初始初始化
String rootDir = MMKV.initialize(dir);

// 根据业务区别存储, 附带一个自己的 ID
MMKV kv2 = MMKV.mmkvWithID("MyID");

MMKV的实例可以默认生成,也可以根据不同的业务需求实例化,

// 默认MMKV 实例化
MMKV kv = MMKV.defaultMMKV();
// 根据业务区别存储, 附带一个自己的 ID
MMKV kv2 = MMKV.mmkvWithID("MyID");
// 多进程同步支持
MMKV kv3 = MMKV.mmkvWithID("MyID", MMKV.MULTI_PROCESS_MODE);

3. 迁移
MMKV支持完美无损的将SP中保存的数据迁移到MMKV中

    private void importSharedPreferences(Context context) {
        MMKV mmkv = MMKV.mmkvWithID("myData");
        SharedPreferences old_man = context.getSharedPreferences("myData", Context.MODE_PRIVATE);
        // 迁移旧数据
        mmkv.importFromSharedPreferences(old_man);
        // 清空旧数据
        old_man.edit().clear().commit();
    }

4. 代码MMKVUtils
MMKV使用非常简单,已封装成一个Utils工具,大家可以复制拿去使用

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

import com.tencent.mmkv.MMKV;

/**
 * @author ken.luo
 */
public class MMKVUtils {
    private static MMKV kv;
    /**
     * 初始化
     * @param context  上下文对象
     */
    public static void init(Context context) {
        String rootDir = MMKV.initialize(context);
//        System.out.println("mmkv root: " + rootDir);
        kv = MMKV.defaultMMKV();
        // 根据业务区别存储, 附带一个自己的 ID
        MMKV kv2 = MMKV.mmkvWithID("MyID");
        // 多进程同步支持
        MMKV kv3 = MMKV.mmkvWithID("MyID", MMKV.MULTI_PROCESS_MODE);
    }

    /**
     * 指定保存位置初始化
     * @param dir   dir = getFilesDir().getAbsolutePath() + "/mmkv_2";
     */
    public static void init(String dir) {
        String rootDir = MMKV.initialize(dir);
        kv = MMKV.defaultMMKV();
    }

    /**
     * Ashmem匿名内存
     * @param context 上下文
     */
    public static void initAshmem(Context context){
        kv = MMKV.mmkvWithAshmemID(context, "Ashmem", 1024,
                MMKV.MULTI_PROCESS_MODE, "553646546546");
    }

    /**
     * 迁移SP 到 MMKV
     */
    private void importSharedPreferences(Context context) {
        MMKV mmkv = MMKV.mmkvWithID("myData");
        SharedPreferences old_man = context.getSharedPreferences("myData", Context.MODE_PRIVATE);
        // 迁移旧数据
        mmkv.importFromSharedPreferences(old_man);
        // 清空旧数据
        old_man.edit().clear().commit();
    }

    public static void putBoolean(String key, boolean value) {
        kv.encode(key, value);
    }

    public static Boolean getBoolean(String key) {
        return kv.decodeBool(key, false);
    }

    public static Boolean getBoolean(String key, boolean defValue) {
        return kv.decodeBool(key, defValue);
    }

    public static void putInteger(String key, int value) {
        kv.encode(key, value);
    }

    public static int getInteger(String key) {
        return kv.decodeInt(key, 1);
    }

    public static int getInteger(String key, int defValue) {
        return kv.decodeInt(key, defValue);
    }

    public static void putString(String key, String value) {
        kv.encode(key, value);
    }

    public static String getString(String key) {
        return kv.decodeString(key, "");
    }

    public static String getString(String key, String defaultValue) {
        return kv.decodeString(key, defaultValue);
    }

    /**
     * 移除数据Key
     * @param key  数据Key
     */
    public static void removeKey(String key) {
        // 删除数据
        kv.removeValueForKey(key);
//        mmkv.removeValueForKey("String")
//        mmkv.removeValuesForKeys(arrayOf("int","bool"))
//        mmkv.containsKey("String")
    }
}


MMKV 、 SharedPreferences、SQLite 对比

单进程性能
可见,MMKV 在写入性能上远远超越 SharedPreferences & SQLite,在读取性能上也有相近或超越的表现。
Android MMKV框架引入使用_第2张图片

多进程性能
MMKV 无论是在写入性能还是在读取性能,都远远超越 MultiProcessSharedPreferences & SQLite & SQLite, MMKV 在 Android 多进程 key-value 存储组件上是不二之选。
Android MMKV框架引入使用_第3张图片

优点

  1. MMKV使用了protobuf作为序列化的数据结构,相比json和xml效率更快,空间占用更低。
  2. 使用了mmap和匿名Ashmem共享内存,减少了用户空间数据到内核空间的拷贝,提高存储效率和安全性
  3. 需要磁盘空间替换内存空间时,就可以使用MMKV
  4. 支持多进程,多进程安全
  5. MMKV包含进程锁、编码/解码帮助程序和mmap逻辑等等代码,新增MMKV框架一般只会带来60K左右的大小。

缺点:

  1. 在某些情况下读操作会比SP慢(查询数据时存在ProtocolBuffer解码;首次实例化会进行数据的复写剔除重复数据)
  2. 在随机写很多的情况下,会导致随机IO操作,导致效率下降。
  3. 不建议太大文件数据存储,会比较快消耗虚拟内存。尽量保证每一个文件存储的数据较小。

MMKV使用注意

  1. 保证每一个文件存储的数据都比较小,也就说需要把数据根据业务线存储分散。这要就不会把虚拟内存消耗过快。

  2. 还需要在适当的时候释放一部分内存数据,比如在App中监听onTrimMemory方法,在Java内存吃紧的情况下进行MMKV的trim操作(不准确,我们暂时以此为信号,最好自己监听进程中内存使用情况)。

  3. 适当的时候释放一部分内存数据,在不需要使用的时候,最好把MMKV给close掉,甚至调用exit方法。


SharedPreferences 迁移

MMKV 提供了 importFromSharedPreferences() 函数,可以比较方便地迁移数据过来,在前面已有迁移代码示例。

MMKV 还额外实现了一遍 SharedPreferences、SharedPreferences.Editor 这两个 interface,在迁移的时候只需两三行代码即可,其他 CRUD 操作代码都不用改。

SharedPreferences缺点:

  1. 跨进程不安全 就算使用了MODE_MULTI_PROCESS,频繁的写入还是会会造成数据丢失。(文件跨进程共享

  2. 加载缓慢 SharedPreferences使用异步加载,由于线程没有设置优先级,按照默认的线程优先级会造成时间片抢占机会小导致主线程长时间的等待。

  3. 全量写入无论是调用 commit() 还是 apply(),即使我们只改动其中的一个条目,都会把整个内容全部写到文件,写入效率低下。

  4. 每次都需要将所有的数据加载到内存,如果存储大量数据,会占用很多的内存。

  5. 卡顿由于提供了异步落盘的 apply 机制,在崩溃或者其他一些异常情况可能会导致数据丢失。所以当应用收到系统广播,或者被调用 onPause 等一些时机,系统会强制把所有的 SharedPreferences 对象数据落地到磁盘。如果没有落地完成,这时候主线程会被一直阻塞。这样非常容易造成卡顿,甚至是 ANR,从线上数据来看 SP卡顿占比一般会超过 5%

  6. 通过 getSharedPreferences 可以获取 SP 实例,从首次初始化到读到数据会存在延迟,因为读文件的操作阻塞调用的线程直到文件读取完毕,如果在主线程调用,可能会对 UI 流畅度造成影响。(线程阻塞)

  7. 将数据写入文件需要将数据拷贝两次,再写入到文件中,如果数据量过大,也会有很大的性能损耗。(二次写入


总结

MMKV作为一种高性能大量数据的存储组件,对比Android传统的存储方式SharedPreferences和SQLite确实有不少优势。核心是使用mmap内存映射文件,对比传统IO,在性能上有很大优势,并且将读写文件的操作变得和操作内存一样简单。

MMKV引入增量写入,重整内存,通过文件大小校验对多进程操作感知,多进程读写锁等等。但它的缺点是可能造成内存的浪费,因为必须映射内存页的整数倍,如果只存储很少量的数据,则显得大材小用。因此,可以作为一种数据存储的选择方案,在一些需要大量存储数据场景时,替代SharedPreferences。

项目 评价 描述
正确性 支持多进程安全, 使用 mmap, 由操作系统保证数据回写的正确性
时间开销 使用 mmap 实现, 减少了用户空间数据到内核空间的拷贝
空间开销 使用 protocl buffer 存储数据, 同样的数据会比 xml 和 json 消耗空间小 使用的是数据追加到末尾的方式, 只有到达一定阈值之后才会触发键值合并, 不合并之前会导致同一个 key 存在多份
安全 使用 crc 校验, 甄别文件系统和操作系统不稳定导致的异常数据
开发成本 使用方式较为简单
兼容性 各个安卓版本都前后兼容

相关链接

  1. Lottie动画 轻松使用
  2. PNG、JPG等普通图片高保真转SVG图
  3. Android 完美的蒙层方案
  4. Android MMKV框架引入使用
  5. 强大无匹的自定义下拉列表
  6. Google Protobuf 实践使用开发

扩展链接:

  1. Android CameraX 使用入门
  2. Android 今日头条屏幕适配详细使用攻略
  3. Android 史上最新最全的ADB及命令百科,没有之一
  4. Android 内存映射mmap浅谈

博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !

你可能感兴趣的:(Android开发框架,android,android,studio,MMKV)