MMKV浅析
MMKV 原理篇
一、内存准备
通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
mmap主要有2种用法,一个是建立匿名映射,可以起到父子进程之间共享内存的作用。另一个是磁盘文件映射进程的虚拟地址空间。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 失败说明其他进程持有了读锁,我们需要先将自己的读锁释放掉,再进行加写锁操作,以避免死锁的发生。
- 解写锁时,假如之前曾经持有读锁,那么我们不能直接释放掉写锁,这样会导致读锁也解了。我们应该加一个读锁,将锁降级。
- 写指针的同步 我们可以在每个进程内部缓存自己的写指针,然后在写入键值的同时,还要把最新的写指针位置也写到 mmap 内存中;这样每个进程只需要对比一下缓存的指针与 mmap 内存的写指针,如果不一样,就说明其他进程进行了写操作。事实上 MMKV 原本就在文件头部保存了有效内存的大小,这个数值刚好就是写指针的内存偏移量,我们可以重用这个数值来校对写指针。
- 内存重整的感知 考虑使用一个单调递增的序列号,每次发生内存重整,就将序列号递增。将这个序列号也放到 mmap 内存中,每个进程内部也缓存一份,只需要对比序列号是否一致,就能够知道其他进程是否触发了内存重整。
- 内存增长的感知 事实上 MMKV 在内存增长之前,会先尝试通过内存重整来腾出空间,重整后还不够空间才申请新的内存。所以内存增长可以跟内存重整一样处理。至于新的内存大小,可以通过查询文件大小来获得,无需在 mmap 内存另外存放。
Protobuf 变长编码原理
原理:
1.可变长度编码 & 跳过可选字段
2.作用于网络传输过程
一、存储类型
TAG_[LENGTH]_VALUE
TAG: filedId(前五位)+ WIRE_TYPE(低三位)
LENGTH: WIRE_TYPE = 2 时 存在
VALUE:WIRE_TYPE = varint 时 采用小端存储模式,其他正常读取
二、WIRE_TYPE
0: varint变长编码,主要就是依靠这个来减小存储体积
1:定长 8byte
2: 指定长度
3、4 :已废弃
5:定长 4 byte
三、Varint 原理
-
int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。
-
采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息
-
小端存储模式
示例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
n | hex | h(n) | ZigZag (hex) |
---|---|---|---|
0 | 00 00 00 00 | 00 00 00 00 | 00 |
-1 | ff ff ff ff | 00 00 00 01 | 01 |
1 | 00 00 00 01 | 00 00 00 02 | 02 |
-2 | ff ff ff fe | 00 00 00 03 | 03 |
2 | 00 00 00 02 | 00 00 00 04 | 04 |
... | ... | ... | ... |
-64 | ff ff ff c0 | 00 00 00 7f | 7f |
64 | 00 00 00 40 | 00 00 00 80 | 80 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 空间
转载自:https://juejin.cn/post/6946127092341964830