likes
comments
collection
share

MMKV浅析

作者站长头像
站长
· 阅读数 1

MMKV 原理篇

一、内存准备

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

mmap主要有2种用法,一个是建立匿名映射,可以起到父子进程之间共享内存的作用。另一个是磁盘文件映射进程的虚拟地址空间。MMKV就是用的磁盘文件映射。

MMKV浅析

普通读写数据:用户空间 ----内核空间--- 磁盘

MMAP: 用户空间----磁盘

二、数据组织

数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。考虑到我们要提供的是通用 kv 组件,key 可以限定是 string 字符串类型,value 则多种多样(int/bool/double 等)。要做到通用的话,考虑将 value 通过 protobuf 协议序列化成统一的内存块(buffer),然后就可以将这些 KV 对象序列化到内存中。

三、写入优化

标准 protobuf 不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力:将增量 kv 对象序列化后,直接 append 到内存末尾;这样同一个 key 会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的

四、空间优化

使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。我们需要在性能和空间上做个折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。

  • 数据的重写时机
    • 文件剩余空间少于新的键值对大小
    • 散列为空
  • 文件扩容时机
    • 所需空间的 1.5 倍超过了当前文件的总大小时, 扩容为之前的两倍s

五、数据有效性

提供文件CRC校验和长度校验及恢复机制,但恢复数据往往不可靠。

六、加密存储

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

七、多进程

1.ContentProvider

(1)单独进程管理数据,数据同步不易出错,简单好用易上手

( 2)将文件 mmap 到每个访问进程的内存空间

2.进程锁 文件锁

(1)递归锁:如果一个进程/线程已经拥有了锁,那么后续的加锁操作不会导致卡死,并且解锁 也不会导致外层的锁被解掉。对于文件锁来说,前者是满足的,后者则不然。因为文件锁是状态锁,没有计数器,无论加了多少次锁,一个解锁操作就全解掉。只要用到子函数,就非常需要递归锁。

(2)锁升级/降级 锁升级是指将已经持有的共享锁,升级为互斥锁,亦即将读锁升级为写锁;锁降级则 是反过来。文件锁支持锁升级,但是容易死锁:假如 A、B 进程都持有了读锁,现在都想升级到写锁,就会陷入相互等待的困境,发生死锁。另外,由于文件锁不支持递归锁,也导致了锁降级无法进行,一降就降到没有锁

  • 加写锁时,如果当前已经持有读锁,那么先尝试加写锁,try_lock 失败说明其他进程持有了读锁,我们需要先将自己的读锁释放掉,再进行加写锁操作,以避免死锁的发生。
  • 解写锁时,假如之前曾经持有读锁,那么我们不能直接释放掉写锁,这样会导致读锁也解了。我们应该加一个读锁,将锁降级。
  1. 写指针的同步 我们可以在每个进程内部缓存自己的写指针,然后在写入键值的同时,还要把最新的写指针位置也写到 mmap 内存中;这样每个进程只需要对比一下缓存的指针与 mmap 内存的写指针,如果不一样,就说明其他进程进行了写操作。事实上 MMKV 原本就在文件头部保存了有效内存的大小,这个数值刚好就是写指针的内存偏移量,我们可以重用这个数值来校对写指针。
  2. 内存重整的感知 考虑使用一个单调递增的序列号,每次发生内存重整,就将序列号递增。将这个序列号也放到 mmap 内存中,每个进程内部也缓存一份,只需要对比序列号是否一致,就能够知道其他进程是否触发了内存重整。
  3. 内存增长的感知 事实上 MMKV 在内存增长之前,会先尝试通过内存重整来腾出空间,重整后还不够空间才申请新的内存。所以内存增长可以跟内存重整一样处理。至于新的内存大小,可以通过查询文件大小来获得,无需在 mmap 内存另外存放。

Protobuf 变长编码原理

原理:

1.可变长度编码 & 跳过可选字段

2.作用于网络传输过程

一、存储类型

TAG_[LENGTH]_VALUE

TAG: filedId(前五位)+ WIRE_TYPE(低三位)

LENGTH: WIRE_TYPE = 2 时 存在

VALUE:WIRE_TYPE = varint 时 采用小端存储模式,其他正常读取

二、WIRE_TYPE

MMKV浅析

0: varint变长编码,主要就是依靠这个来减小存储体积

1:定长 8byte

2: 指定长度

3、4 :已废弃

5:定长 4 byte

三、Varint 原理

  1. int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。

  2. 采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息

  3. 小端存储模式

    示例1:

    对于数字1 对应的二进制是 :00000000 00000000 00000000 00000001

    PB 只用一个字节就可以存储该值:即 0 00000001

    第一位0 表示该字节就是结束字节,后七位 0000001 即表示十进制的数字 1

    示例2:

    对于数字 500,对应的二进制:00000000 00000000 00000001 11110100

​ 从最低位开始 七位分割 即: 1110100 0000011

​ PB编码用两个字节表示 : 1 1110100 0 0000011

​ 解码: 高位是 1 表示 还要读后面一个字节

​ 去掉最高位:1110100 0000011

​ 由于是小端模式:组合后是 00000111110100 十进制即是500

三、zigTag 编码(解决负数占用多字节问题)

  • 原码:最高位为符号位,剩余位表示绝对值;

  • 反码:除符号位外,对原码剩余位依次取反;

  • 补码:对于正数,补码为其自身;对于负数,除符号位外对原码剩余位依次取反然后+1

    原码缺陷:

    1、 0 有两种表现形式 : 00000000 和 10000000

    2、计算错误:1 + (-1) = 00000001 + 10000001 = 10000010 = -2

补码解决的问题:1+(-1) = 00000001+ 11111111 = 00000000 = 0

nhexh(n)ZigZag (hex)
000 00 00 0000 00 00 0000
-1ff ff ff ff00 00 00 0101
100 00 00 0100 00 00 0202
-2ff ff ff fe00 00 00 0303
200 00 00 0200 00 00 0404
............
-64ff ff ff c000 00 00 7f7f
6400 00 00 4000 00 00 8080 01
............

拿到hash值后,想当然的编码策略:直接去掉hash值的前导0之后的byte作为压缩编码。

四、示例

示例1:

message Test1 {

​ optional int32 a = 1; //表明是 fildId 是 1

}

创建 Test1消息并把a设置为150

编码:08 96 01

解码:00001000 10010110 00000001

(1)000001000 后三位:000 表示WIRE_TYPE = 0,即 Varint;00001 = 1 表示对应第一个 fieldId;

(2)10010110 00000001:

10010110 最高位 1 表示还需要读取下一个字节,剩余 0010110;

00000001 最高位表示无需读下一个字节,剩余 0000001;

按照小端模式进行拼接:00000010010110 转化为10进制 即 150

示例2:内嵌消息

Message Test2{

​ optional Test1 c = 3;

}

如示例1 给Test1 a 赋值 150

则得到的编码:1a 03 08 96 01

1a 对应 2进制:00011010 后三位表示 有线类型 2,前五位表示 3 对应的 是fieldId

03 表示长度:及3个字节的长度

08 96 01 : 见示例一分析过程


MMKV源码解析篇(java层)

一、成员变量

//错误恢复策略相关
private static final EnumMap<MMKVRecoverStrategic, Integer> recoverIndex = new EnumMap(MMKVRecoverStrategic.class);
//MMKV日志输出相关
private static final EnumMap<MMKVLogLevel, Integer> logLevel2Index;
private static final MMKVLogLevel[] index2LogLevel;
//存储已经打开的 MMKV 描述符集合
private static final Set<Long> checkedHandleSet;
//MMKV 文件存放根路径
private static String rootDir;
//MMKV 进程模式
public static final int SINGLE_PROCESS_MODE = 1;
public static final int MULTI_PROCESS_MODE = 2;
private static final int CONTEXT_MODE_MULTI_PROCESS = 4;
private static final int ASHMEM_MODE = 8;
//序列化相关的 
private static final HashMap<String, Creator<?>> mCreators;
//处理日志重定向、文件错误恢复策略
private static MMKVHandler gCallbackHandler;
private static boolean gWantLogReDirecting;
//内容变更通知 主要是被其他进程更新时有回调
private static MMKVContentChangeNotification gContentChangeNotify;
//MMKV描述符 主要用来读写文件用
private final long nativeHandle;

二、API接口

1.初始化MMKV

//传入context的初始化函数,log默认是LevelInfo
public static String initialize(Context context) {
    String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
    MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
    return initialize((String)root, (MMKV.LibLoader)null, logLevel);
}
//传入context 和 logLevel 
public static String initialize(Context context, MMKVLogLevel logLevel) {
    String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
    return initialize((String)root, (MMKV.LibLoader)null, logLevel);
}
//传入context 和 loader
public static String initialize(Context context, MMKV.LibLoader loader) {
    String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
    MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
    return initialize(root, loader, logLevel);
}
// 传入 context 、loader/level
public static String initialize(Context context, MMKV.LibLoader loader, MMKVLogLevel logLevel) {
    String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
    return initialize(root, loader, logLevel);
}
// 传入路径
public static String initialize(String rootDir) {
    MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
    return initialize((String)rootDir, (MMKV.LibLoader)null, logLevel);
}
//传入路径和日志等级
public static String initialize(String rootDir, MMKVLogLevel logLevel) {
    return initialize((String)rootDir, (MMKV.LibLoader)null, logLevel);
}
//传入路径和loader
public static String initialize(String rootDir, MMKV.LibLoader loader) {
    MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
    return initialize(rootDir, loader, logLevel);
}
//最后都调用的该方法  传入路径 /loader 和 日志等级
public static String initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel) {
    //如果loader不为空,则由传入的loader 加载 c++_shared 和 mmkv 两个so库,否则由System.loadLibrary 加载
    if (loader != null) {
        if ("SharedCpp".equals("SharedCpp")) {
            loader.loadLibrary("c++_shared");
        }

        loader.loadLibrary("mmkv");
    } else {
        if ("SharedCpp".equals("SharedCpp")) {
            System.loadLibrary("c++_shared");
        }

        System.loadLibrary("mmkv");
    }
    //jni 调用native方法
    jniInitialize(rootDir, logLevel2Int(logLevel));
    //记录rootDir
    MMKV.rootDir = rootDir;
    return MMKV.rootDir;
}

总结:初始化最后都调用的是 initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel)

2.获取MMKV存储根路径

//可以根据此方法判断路径是否为空 是否初始化成功
public static String getRootDir() {
    return rootDir;
}

3.获取MMKV

//通过mapId 获取,默认单进程 获取前必须保证初始化 即路径不为空
@Nullable
public static MMKV mmkvWithID(String mmapID) {
    if (rootDir == null) {
        throw new IllegalStateException("You should Call MMKV.initialize() first.");
    } else {
        long handle = getMMKVWithID(mmapID, 1, (String)null, (String)null);
        return checkProcessMode(handle, mmapID, 1);
    }
}

//通过mapId 、进程mode 获取
@Nullable
public static MMKV mmkvWithID(String mmapID, int mode) {
    if (rootDir == null) {
        throw new IllegalStateException("You should Call MMKV.initialize() first.");
    } else {
        long handle = getMMKVWithID(mmapID, mode, (String)null, (String)null);
        return checkProcessMode(handle, mmapID, mode);
    }
}
//通过mapId mode 、key 获取
@Nullable
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey) {
    if (rootDir == null) {
        throw new IllegalStateException("You should Call MMKV.initialize() first.");
    } else {
        long handle = getMMKVWithID(mmapID, mode, cryptKey, (String)null);
        return checkProcessMode(handle, mmapID, mode);
    }
}
//通过mapID 和 rootpath 获取
@Nullable
public static MMKV mmkvWithID(String mmapID, String rootPath) {
    if (rootDir == null) {
        throw new IllegalStateException("You should Call MMKV.initialize() first.");
    } else {
        long handle = getMMKVWithID(mmapID, 1, (String)null, rootPath);
        return checkProcessMode(handle, mmapID, 1);
    }
}
//通过 mapId  rootpath  进程、key 获取
@Nullable
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, String rootPath) {
    if (rootDir == null) {
        throw new IllegalStateException("You should Call MMKV.initialize() first.");
    } else {
        long handle = getMMKVWithID(mmapID, mode, cryptKey, rootPath);
        return checkProcessMode(handle, mmapID, mode);
    }
}
//匿名共享内存方式获取  跨进程传输 MMKV对应的 handle /size/ mode/key
@Nullable
public static MMKV mmkvWithAshmemID(Context context, String mmapID, int size, int mode, @Nullable String cryptKey) {
    if (rootDir == null) {
        throw new IllegalStateException("You should Call MMKV.initialize() first.");
    } else {
        String processName = MMKVContentProvider.getProcessNameByPID(context, Process.myPid());
        if (processName != null && processName.length() != 0) {
            if (processName.contains(":")) {
                Uri uri = MMKVContentProvider.contentUri(context);
                if (uri == null) {
                    simpleLog(MMKVLogLevel.LevelError, "MMKVContentProvider has invalid authority");
                    return null;
                } else {
                    simpleLog(MMKVLogLevel.LevelInfo, "getting parcelable mmkv in process, Uri = " + uri);
                    Bundle extras = new Bundle();
                    extras.putInt("KEY_SIZE", size);
                    extras.putInt("KEY_MODE", mode);
                    if (cryptKey != null) {
                        extras.putString("KEY_CRYPT", cryptKey);
                    }

                    ContentResolver resolver = context.getContentResolver();
                    Bundle result = resolver.call(uri, "mmkvFromAshmemID", mmapID, extras);
                    if (result != null) {
                        result.setClassLoader(ParcelableMMKV.class.getClassLoader());
                        ParcelableMMKV parcelableMMKV = (ParcelableMMKV)result.getParcelable("KEY");
                        if (parcelableMMKV != null) {
                            MMKV mmkv = parcelableMMKV.toMMKV();
                            if (mmkv != null) {
                                simpleLog(MMKVLogLevel.LevelInfo, mmkv.mmapID() + " fd = " + mmkv.ashmemFD() + ", meta fd = " + mmkv.ashmemMetaFD());
                            }

                            return mmkv;
                        }
                    }

                    return null;
                }
            } else {
                simpleLog(MMKVLogLevel.LevelInfo, "getting mmkv in main process");
                mode |= 8;
                long handle = getMMKVWithIDAndSize(mmapID, size, mode, cryptKey);
                return new MMKV(handle);
            }
        } else {
            simpleLog(MMKVLogLevel.LevelError, "process name detect fail, try again later");
            return null;
        }
    }
}
//获取默认的存储MMKV 
@Nullable
public static MMKV defaultMMKV() {
    if (rootDir == null) {
        throw new IllegalStateException("You should Call MMKV.initialize() first.");
    } else {
        //默认获取的mode是单进程,不加密,默认获取的mapId 是DefaultMMKV
        long handle = getDefaultMMKV(1, (String)null);
        return checkProcessMode(handle, "DefaultMMKV", 1);
    }
}
//获取默认的存储文件 
@Nullable
public static MMKV defaultMMKV(int mode, @Nullable String cryptKey) {
    if (rootDir == null) {
        throw new IllegalStateException("You should Call MMKV.initialize() first.");
    } else {
        long handle = getDefaultMMKV(mode, cryptKey);
        return checkProcessMode(handle, "DefaultMMKV", mode);
    }
}

//检查进程模式  handle类似句柄不能为0,校验进程模式,如果模式不匹配,会抛异常
//handle 会存入本地的set,如果本地无记录handle, 则将当前模式和handle进行校验,不匹配则报错,抛出异常
//模式校验通过,则将handle 赋值给变量值 private final long nativeHandle,即new MMKV();
//这里的疑问见后续章节分析,发现如果用不同进程模式取获取同一文件名,获取到的handle值是一样的,并未报错
@Nullable
private static MMKV checkProcessMode(long handle, String mmapID, int mode) {
    if (handle == 0L) {
        return null;
    } else {
        if (!checkedHandleSet.contains(handle)) {
            if (!checkProcessMode(handle)) {
                String message;
                if (mode == 1) {
                    message = "Opening a multi-process MMKV instance [" + mmapID + "] with SINGLE_PROCESS_MODE!";
                } else {
                    message = "Opening a MMKV instance [" + mmapID + "] with MULTI_PROCESS_MODE, ";
                    message = message + "while it's already been opened with SINGLE_PROCESS_MODE by someone somewhere else!";
                }

                throw new IllegalArgumentException(message);
            }

            checkedHandleSet.add(handle);
        }

        return new MMKV(handle);
    }
}

4.功能接口

/****退出功能******/

//退出MMKV,针对的是全局MMKV,退出后必须重新调用initialize 否则再获取MMKV 实例时会抛异常 
public static native void onExit();
//当某个MMKV实例不再使用时 可以考虑调用此方法关掉,后续需要重新获取该实例 
public native void close();

/***操作 加解密 相关功能***/

//获取秘钥针对的是单个MMKV实例
@Nullable
public native String cryptKey();
//重置秘钥,针对单个MMKV文件,返回重置成功或者失败
public native boolean reKey(@Nullable String var1);
//待验证
public native void checkReSetCryptKey(@Nullable String var1);

/*****获取size相关功能****/

//静态方法 获取pagesize  返回默认值 4K
public static native int pageSize();
//获取某个key 对应的value的size  需要结合PB编码 比如整型 1 PB编码后获取到的是 1,500获取到的是 2,对于引用类型比如字符串,由于PB编码会插入字节表示长度,这里获取的大小是包含长度字节的,比如“12345”,这里获取到是6
public int getValueSize(String key) {
    return this.valueSize(this.nativeHandle, key, false);
}
//获取某个key 对应的value的实际size 需要结合PB编码 大部分情况下获取到的值和上述 getValueSize值是一样的,但对于字符串类型 比如“12345”获取到的是实际大小 5,会去掉表示长度的字节
public int getValueActualSize(String key) {
    return this.valueSize(this.nativeHandle, key, true);
}
//该存储所占用大小 返回的是 pagesize 的倍数 
public long totalSize() {
    return this.totalSize(this.nativeHandle);
}
//测试结果是 Key的数量(不含重复项);removeValueForkey 也会删除key,clearALL 后变为0
public long count() {
    return this.count(this.nativeHandle);
}

/***操作key相关功能***/

//是否包含某个key
public boolean containsKey(String key) {
    return this.containsKey(this.nativeHandle, key);
}
//删除某个key 对应的value,同时key 也会删除
public void removeValueForKey(String key) {
    this.removeValueForKey(this.nativeHandle, key);
}
@Nullable
//获取所有key
public native String[] allKeys();
//删除相关key对应的value
public native void removeValuesForKeys(String[] var1);

/***清除功能***/
//清除所有数据 针对单个MMKV
public native void clearAll();
//触发对齐 当删除很多key_value后强制对齐下  
public native void trim();
//清除缓存 由于和SP一样的机制,会读缓存加载所有keyvalue,内存告警时可以清除,再次使用时会加载所有
public native void clearMemoryCache();

//获取版本号
public static native String version();
//获取文件名即mmapId
public native String mmapID();
/****锁相关 多进程时使用*****/
//加锁
public native void lock();
//解锁
public native void unlock();
//尝试解锁
public native boolean tryLock();

//文件是否有效
public static native boolean isFileValid(String var0);

/**强制触发一次同步 写文件**/
//一般情况下不要调用 除非你担心 电池没电了  
public void sync() {
   this.sync(true);
}
//
public void async() {
   this.sync(false);
}
//
private native void sync(boolean var1);
/***共享内存相关***/
public native int ashmemFD();

public native int ashmemMetaFD();

/**当从 MMKV 取一个 String or byte[]的时候,会有一次从 native 到 JVM 的内存拷贝。如果这个值立即传递到另*一个 native 库(JNI),又会有一次从 JVM 到 native 的内存拷贝。当这个值比较大的时候,整个过程会非常浪费。*Native Buffer 就是为了解决这个问题。
*Native Buffer 是由 native 创建的内存缓冲区,在 Java 里封装成 NativeBuffer 类型,可以透明传递到另一个 *native 库进行访问处理。整个过程避免了先拷内存到 JVM 又从 JVM 拷回来导致的浪费
**/
public static NativeBuffer createNativeBuffer(int size) {
    long pointer = createNB(size);
    return pointer <= 0L ? null : new NativeBuffer(pointer, size);
}

public static void destroyNativeBuffer(NativeBuffer buffer) {
    destroyNB(buffer.pointer, buffer.size);
}

public int writeValueToNativeBuffer(String key, NativeBuffer buffer) {
    return this.writeValueToNB(this.nativeHandle, key, buffer.pointer, buffer.size);
}
//多进程通知监听
private static native void setWantsContentChangeNotify(boolean var0);

public native void checkContentChangedByOuterProcess();

5.写/读数据

支持的类型:bool/float/string/byte[]/int/long/double/Set/Parcelable

//读写bool类型相关
public boolean encode(String key, boolean value) {
    return this.encodeBool(this.nativeHandle, key, value);
}
public boolean decodeBool(String key) {
    return this.decodeBool(this.nativeHandle, key, false);
}

public boolean decodeBool(String key, boolean defaultValue) {
    return this.decodeBool(this.nativeHandle, key, defaultValue);
}
//读写int 类型
public boolean encode(String key, int value) {
    return this.encodeInt(this.nativeHandle, key, value);
}

public int decodeInt(String key) {
    return this.decodeInt(this.nativeHandle, key, 0);
}

public int decodeInt(String key, int defaultValue) {
    return this.decodeInt(this.nativeHandle, key, defaultValue);
}
//读写long 类型
public boolean encode(String key, long value) {
    return this.encodeLong(this.nativeHandle, key, value);
}

public long decodeLong(String key) {
    return this.decodeLong(this.nativeHandle, key, 0L);
}

public long decodeLong(String key, long defaultValue) {
    return this.decodeLong(this.nativeHandle, key, defaultValue);
}
//读写float类型
public boolean encode(String key, float value) {
    return this.encodeFloat(this.nativeHandle, key, value);
}

public float decodeFloat(String key) {
    return this.decodeFloat(this.nativeHandle, key, 0.0F);
}

public float decodeFloat(String key, float defaultValue) {
    return this.decodeFloat(this.nativeHandle, key, defaultValue);
}
//读写double 类型
public boolean encode(String key, double value) {
    return this.encodeDouble(this.nativeHandle, key, value);
}

public double decodeDouble(String key) {
    return this.decodeDouble(this.nativeHandle, key, 0.0D);
}

public double decodeDouble(String key, double defaultValue) {
    return this.decodeDouble(this.nativeHandle, key, defaultValue);
}
//读写字符串类型
public boolean encode(String key, @Nullable String value) {
    return this.encodeString(this.nativeHandle, key, value);
}

@Nullable
public String decodeString(String key) {
    return this.decodeString(this.nativeHandle, key, (String)null);
}

@Nullable
public String decodeString(String key, @Nullable String defaultValue) {
    return this.decodeString(this.nativeHandle, key, defaultValue);
}
//读写 set<string> 类型
public boolean encode(String key, @Nullable Set<String> value) {
    return this.encodeSet(this.nativeHandle, key, value == null ? null : (String[])value.toArray(new String[0]));
}

@Nullable
public Set<String> decodeStringSet(String key) {
    return this.decodeStringSet(key, (Set)null);
}

@Nullable
public Set<String> decodeStringSet(String key, @Nullable Set<String> defaultValue) {
    return this.decodeStringSet(key, defaultValue, HashSet.class);
}

@Nullable
public Set<String> decodeStringSet(String key, @Nullable Set<String> defaultValue, Class<? extends Set> cls) {
    String[] result = this.decodeStringSet(this.nativeHandle, key);
    if (result == null) {
        return defaultValue;
    } else {
        Set a;
        try {
            a = (Set)cls.newInstance();
        } catch (IllegalAccessException var7) {
            return defaultValue;
        } catch (InstantiationException var8) {
            return defaultValue;
        }

        a.addAll(Arrays.asList(result));
        return a;
    }
}
//读写 byte[]
public boolean encode(String key, @Nullable byte[] value) {
    return this.encodeBytes(this.nativeHandle, key, value);
}

@Nullable
public byte[] decodeBytes(String key) {
    return this.decodeBytes(key, (byte[])null);
}

@Nullable
public byte[] decodeBytes(String key, @Nullable byte[] defaultValue) {
    byte[] ret = this.decodeBytes(this.nativeHandle, key);
    return ret != null ? ret : defaultValue;
}
//读写序列化类型 Parcelable
public boolean encode(String key, @Nullable Parcelable value) {
    if (value == null) {
        return this.encodeBytes(this.nativeHandle, key, (byte[])null);
    } else {
        Parcel source = Parcel.obtain();
        value.writeToParcel(source, value.describeContents());
        byte[] bytes = source.marshall();
        source.recycle();
        return this.encodeBytes(this.nativeHandle, key, bytes);
    }
}

@Nullable
public <T extends Parcelable> T decodeParcelable(String key, Class<T> tClass) {
    return this.decodeParcelable(key, tClass, (Parcelable)null);
}

@Nullable
public <T extends Parcelable> T decodeParcelable(String key, Class<T> tClass, @Nullable T defaultValue) {
    if (tClass == null) {
        return defaultValue;
    } else {
        byte[] bytes = this.decodeBytes(this.nativeHandle, key);
        if (bytes == null) {
            return defaultValue;
        } else {
            Parcel source = Parcel.obtain();
            source.unmarshall(bytes, 0, bytes.length);
            source.setDataPosition(0);

            Parcelable var8;
            try {
                String name = tClass.toString();
                Creator creator;
                synchronized(mCreators) {
                    //先从本地缓存中获取
                    creator = (Creator)mCreators.get(name);
                    if (creator == null) {
                        //获取public 变量  CREATOR,  由于是反射所以会保存到hashmap中 避免耗时
                        Field f = tClass.getField("CREATOR");
                        creator = (Creator)f.get((Object)null);
                        if (creator != null) {
                            mCreators.put(name, creator);
                        }
                    }
                }
                //如果为空 则表明类没有实现Parcelable 序列化
                if (creator == null) {
                    throw new Exception("Parcelable protocol requires a non-null static Parcelable.Creator object called CREATOR on class " + name);
                }

                var8 = (Parcelable)creator.createFromParcel(source);
            } catch (Exception var16) {
                simpleLog(MMKVLogLevel.LevelError, var16.toString());
                return defaultValue;
            } finally {
                source.recycle();
            }

            return var8;
        }
    }
}


//支持的类型:bool/int/byte[]/long/float/double/Parcelable/Set<String>/string;
//数据的读取和写入关键是 nativeHandle 类似文件句柄

6.sp 迁移

public int importFromSharedPreferences(SharedPreferences preferences) {
    //获取sp 存储的所有键值对   这里没有对参数判空
    Map<String, ?> kvs = preferences.getAll();
    if (kvs != null && kvs.size() > 0) {
        Iterator var3 = kvs.entrySet().iterator();

        while(var3.hasNext()) {
            Entry<String, ?> entry = (Entry)var3.next();
            String key = (String)entry.getKey();
            Object value = entry.getValue();
            if (key != null && value != null) {
                if (value instanceof Boolean) {
                    this.encodeBool(this.nativeHandle, key, (Boolean)value);
                } else if (value instanceof Integer) {
                    this.encodeInt(this.nativeHandle, key, (Integer)value);
                } else if (value instanceof Long) {
                    this.encodeLong(this.nativeHandle, key, (Long)value);
                } else if (value instanceof Float) {
                    this.encodeFloat(this.nativeHandle, key, (Float)value);
                } else if (value instanceof Double) {
                    this.encodeDouble(this.nativeHandle, key, (Double)value);
                } else if (value instanceof String) {
                    this.encodeString(this.nativeHandle, key, (String)value);
                } else if (value instanceof Set) {
                    this.encode(key, (Set)value);
                } else {
                    simpleLog(MMKVLogLevel.LevelError, "unknown type: " + value.getClass());
                }
            }
        }
        //返回SP键值对的数量
        return kvs.size();
    } else {
        return 0;
    }
}

//sp 数据变更监听 这里MMKV没有做实现,调用会抛异常 谨慎调用
 public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        throw new UnsupportedOperationException("Not implement in MMKV");
    }

    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        throw new UnsupportedOperationException("Not implement in MMKV");
    }


7.日志定向/数据恢复

//默认的log等级是levelInfo,转换成相应等级
private static int logLevel2Int(MMKVLogLevel level) {
    byte realLevel;
    switch(level) {
    case LevelDebug:
        realLevel = 0;
        break;
    case LevelWarning:
        realLevel = 2;
        break;
    case LevelError:
        realLevel = 3;
        break;
    case LevelNone:
        realLevel = 4;
        break;
    case LevelInfo:
    default:
        realLevel = 1;
    }

    return realLevel;
}

public static void setLogLevel(MMKVLogLevel level) {
    int realLevel = logLevel2Int(level);
    //设置给MMKV底层库
    setLogLevel(realLevel);
}
//注册gCallbackHandler
public static void registerHandler(MMKVHandler handler) {
    gCallbackHandler = handler;
    if (gCallbackHandler.wantLogRedirecting()) {
        setCallbackHandler(true, true);
        gWantLogReDirecting = true;
    } else {
        setCallbackHandler(false, true);
        gWantLogReDirecting = false;
    }

}
//解注册gCallbackHandler
public static void unregisterHandler() {
    gCallbackHandler = null;
    setCallbackHandler(false, false);
    gWantLogReDirecting = false;
}

//文件CRC校验错误时 默认采用丢弃策略,如果设置了gCallbackHandler,则采用设置的策略,要么丢弃,要么恢复,恢复不可信
private static int onMMKVCRCCheckFail(String mmapID) {
    MMKVRecoverStrategic strategic = MMKVRecoverStrategic.OnErrorDiscard;
    if (gCallbackHandler != null) {
        strategic = gCallbackHandler.onMMKVCRCCheckFail(mmapID);
    }

    simpleLog(MMKVLogLevel.LevelInfo, "Recover strategic for " + mmapID + " is " + strategic);
    Integer value = (Integer)recoverIndex.get(strategic);
    return value == null ? 0 : value;
}
//文件长度错误时 默认采用丢弃策略,如果设置了gCallbackHandler,则采用设置的策略,要么丢弃要么恢复,恢复不可信
private static int onMMKVFileLengthError(String mmapID) {
    MMKVRecoverStrategic strategic = MMKVRecoverStrategic.OnErrorDiscard;
    if (gCallbackHandler != null) {
        strategic = gCallbackHandler.onMMKVFileLengthError(mmapID);
    }

    simpleLog(MMKVLogLevel.LevelInfo, "Recover strategic for " + mmapID + " is " + strategic);
    Integer value = (Integer)recoverIndex.get(strategic);
    return value == null ? 0 : value;
}
//如果gCallbackHandler 设置了日志重定向 则重定向日志由业务方接管,否则MMKV采用默认的Log 输出
private static void mmkvLogImp(int level, String file, int line, String function, String message) {
    if (gCallbackHandler != null && gWantLogReDirecting) {
        gCallbackHandler.mmkvLog(index2LogLevel[level], file, line, function, message);
    } else {
        switch(index2LogLevel[level]) {
        case LevelDebug:
            Log.d("MMKV", message);
            break;
        case LevelWarning:
            Log.w("MMKV", message);
            break;
        case LevelError:
            Log.e("MMKV", message);
        case LevelNone:
        default:
            break;
        case LevelInfo:
            Log.i("MMKV", message);
        }
    }

}
//获取堆栈信息 组织log  函数内没有做越界判断  
private static void simpleLog(MMKVLogLevel level, String message) {
    StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
    StackTraceElement e = stacktrace[stacktrace.length - 1];
    Integer i = (Integer)logLevel2Index.get(level);
    int intLevel = i == null ? 0 : i;
    mmkvLogImp(intLevel, e.getFileName(), e.getLineNumber(), e.getMethodName(), message);
}
8.数据变化通知(进程间使用)
//注册数据变化监听
public static void registerContentChangeNotify(MMKVContentChangeNotification notify) {
    gContentChangeNotify = notify;
    setWantsContentChangeNotify(gContentChangeNotify != null);
}
//解注册数据变化监听
public static void unregisterContentChangeNotify() {
    gContentChangeNotify = null;
    setWantsContentChangeNotify(false);
}
//其他进程改变数据后通知
private static void onContentChangedByOuterProcess(String mmapID) {
    if (gContentChangeNotify != null) {
        gContentChangeNotify.onContentChangedByOuterProcess(mmapID);
    }

}

private static native void setWantsContentChangeNotify(boolean var0);

MMKV使用注意事项

通过阅读源码或者查阅资料,整理MMKV使用过程中的问题和及调试工具,作为接入前的准备

Q1、哪种接入方式可以缩减APK体积

第一种:implementation 'com.tencent:mmkv:1.2.7'
第二种:implementation 'com.tencent:mmkv-static:1.2.7'

两种接入方式编译后APK会多200K,不采用static 会多出libc++_shared.so 和 libc++.so ,共 415K,而static接入方式只要211K,节省大概一半,可以看到 downloadSize 也会减少200K;

而MMKV官方注释却推荐采用非static 方式接入以减小安装包大小,为了避免底层库冲突,建议采用 static 方式接入

Q2、MMKV是否线程安全?

MMKV 无法保证原子性,所以非线程安全。基于MMKV原理是增量更新,也就是理想情况下最后写入的key 才是最终结果。所以高并发更新同一文件 同一key 需谨慎使用

Q3、MMKV是否类型安全?

使用MMKV 存储一个字符串类型数据 “123”,然后用其他类型获取,都能获取到不同的值,但不会报错,也不会取默认值(同样的其他类型也一样)。

Q4、checkProcessMode?

源码分析见源码分析,这里不再赘述,提出以下疑问:

1.第一次用mapId ,单进程 获取MMKV,handle 已存入set;第二次用多进程、同样的mapId 获取,获取到的handle 和之前相同么?
Case1 : 先用 id = "singleMMkvProcess" 单进程获取MMKV,不杀死app,再用id= "singleMMkvProcess" 多进程获取MMKV, 获取到的MMKV 实例handle 是否一致
结果: handle 一致
Case2 : 先用 id = "singleMMkvProcess" 多进程获取MMKV,不杀死app,再用id= "singleMMkvProcess" 单进程获取MMKV, 获取到的MMKV 实例handle 是否一致
结果: handle 一致
Case3 : 先用 id = "singleMMkvProcess" 单进程获取MMKV,杀死app,再用id= "singleMMkvProcess" 多进程获取MMKV, 获取到的MMKV 实例handle 是否一致
结果:不一致
Case4 : 先用 id = "singleMMkvProcess" 多进程获取MMKV,杀死app,再用id= "singleMMkvProcess" 单进程获取MMKV, 获取到的MMKV 实例handle 是否一致
结果:handle一致
2.如果我第一次用mapId ,带秘钥 获取MMKV;第二次用同样的mapId 不带秘钥获取,获取到的handle 和之前相同么?
Case1 : 先用 id = "singleMMkvProcess" 不加秘钥获取MMKV,不杀死app,再用id= "singleMMkvProcess" 加秘钥获取MMKV, 获取到的MMKV 实例handle 是否一致
结果:handle 一致
Case2 : 先用 id = "singleMMkvProcess" 加秘钥获取MMKV,不杀死app,再用id= "singleMMkvProcess" 不加秘钥获取MMKV, 获取到的MMKV 实例handle 是否一致
结果:handle 一致
Case3 : 先用 id = "singleMMkvProcess" 不加秘钥获取MMKV,杀死app,再用id= "singleMMkvProcess" 加秘钥获取MMKV, 获取到的MMKV 实例handle 是否一致
结果:handle 一致
Case4 : 先用 id = "singleMMkvProcess" 加密获取MMKV,杀死app,再用id= "singleMMkvProcess" 不加密获取MMKV, 获取到的MMKV 实例handle 是否一致
结果:handle不一致
先MMKV加密存储,然后rekey 不加密,不杀死app 的情况下 由于获取handle一致,则获取结果一致;杀死app 情况下,针对第四点handle 不一致,获取不到写入的值
3.注意:尽量减少中途插入秘钥或者改变进程方式获取MMKV

Q5、多进程匿名共享内存的坑?

匿名共享内存(谨慎使用)
public static MMKV mmkvWithAshmemID(Context context, String mmapID, int size, int mode, @Nullable String cryptKey)

原理:内部实现实际使用了 MMKVContentProvider 来传递文件描述符

案例分析:在mmkvdemo进程中使用mmkvWithAshmemID创建一个mmkv实例,主进程创建在两个service进程中同样创建了mmkv实例,这时候测试主进程写一个key的值,两个service进程读取key的值是对的。

android stuido控制台杀掉mmkvdemo进程(两个service进程还在),再打开app进行同样的上述流程,我发现其实主进程的mmkv实例和两个服务进程的mmkv实例映射的不是同一块内存地址了,主进程修改key的值,服务进程读取还是旧的。

对于多进程应用来说,主进程在后台被杀是很正常的现象,所以该中场景存在使用弊端

Q6、是否方便迁移至其他存储?

由于存储时类型擦除,导致无法一次性获取所有数据,无法做到类似SP 一样 实现后续一键迁移

public Map<String, ?> getAll() {
    throw new UnsupportedOperationException("use allKeys() instead, getAll() not implement because type-erasure inside mmkv");
}

Q7、数据存储分了两个文件?

​ 1.两个文件增加了文件校验失败概率

​ 2.数据量很小,也会占用4Kb 空间