持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
说起MMKV,做Android或者iOS原生的小伙伴估计都有听到或者使用过,用于取代SharedPreferences、NSUserDefaults、SQLite 等常见组件
MMKV 原理
- 内存准备
通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。 - 数据组织
数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。 - 写入优化
考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。 - 空间增长
使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
更详细的设计原理参考 MMKV 原理。
这些都是官网上的讲解,并不是本文的要点,本文不是说明原生Android和iOS上的使用,主要讲解的是Flutter For MMKV 。如果错误,还请大家指出
本文讲解基于mmkv 1.2.14
1、源码结构
源码主要分为3个模块:android/ios和源码lib
`android` -> 对于Android的支持
`example` -> 示例工程
`iOS` -> 对于iOS的支持
`tool` -> 对于iOS编译支持
`pubspec.yaml` -> 插件声明文件
2、Android支持分析
对开Android工程的build.gradle文件,我们可以看到脚本里面主要是加入了com.tencent:mmkv-static库的编译
implementation 'com.tencent:mmkv-static:1.2.13'
和原生的Android有所不同,所以如果原生中也使用了MKKV的话,需要按指导上说明的来解决冲突
modules {
module("com.tencent:mmkv") {
replacedBy("com.tencent:mmkv-static", "Using mmkv-static for flutter")
}
}
插件的源码比较简单,只定义了一个方法initializeMMKV用来初始化,但事实上并没有被使用到,因为已经通过ffi走C方法初始化了,不知道是否是特意保留
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("initializeMMKV")) {
final String rootDir = call.argument("rootDir");
final int logLevel = call.argument("logLevel");
final String ret = MMKV.initialize(rootDir, MMKVLogLevel.values()[logLevel]);
result.success(ret);
} else {
result.notImplemented();
}
}
3、iOS支持分析
对于iOS上也会存在冲突To avoid conflict of the native lib name 'libMMKV.so' on iOS, we need to change the plugin name 'mmkv' to 'mmkvflutter'.所以需要脚本改名
mmkvflutter.podspec
s.dependency 'Flutter'
s.dependency 'MMKV', '>= 1.2.13'
s.platform = :ios, '9.0'
iOS相对于Android稍微复杂一点,同样定义了初始化方法。
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
if ([@"initializeMMKV" isEqualToString:call.method]) {
NSString *rootDir = [call.arguments objectForKey:@"rootDir"];
NSNumber *logLevel = [call.arguments objectForKey:@"logLevel"];
NSString *groupDir = [call.arguments objectForKey:@"groupDir"];
NSString *ret = nil;
if (groupDir.length > 0) {
ret = [MMKV initializeMMKV:rootDir groupDir:groupDir logLevel:logLevel.intValue];
} else {
ret = [MMKV initializeMMKV:rootDir logLevel:logLevel.intValue];
}
result(ret);
} else {
result(FlutterMethodNotImplemented);
}
}
不同的是,还定义了mkkv的c函数桥,把需要使用了mkkv函数都在flutter-bridge.mm包裹实现了一遍,由于Dart FFI是获取不到C++风格的符号的,所以我们需要使用C风格函数来操作类。具体可以看以下代码片段
#import <MMKV/MMKV.h>
#import <stdint.h>
#import <string>
#define MMKV_EXPORT extern "C" __attribute__((visibility("default"))) __attribute__((used))
#define MMKV_FUNC(func) mmkv_ ## func
MMKV_EXPORT bool MMKV_FUNC(encodeBool)(const void *handle, const char *oKey, bool value) {
MMKV *kv = (__bridge MMKV *) handle;
if (kv && oKey) {
auto key = [NSString stringWithUTF8String:oKey];
return [kv setBool:value forKey:key];
}
return false;
}
MMKV_EXPORT bool MMKV_FUNC(decodeBool)(const void *handle, const char *oKey, bool defaultValue) {
MMKV *kv = (__bridge MMKV *) handle;
if (kv && oKey) {
auto key = [NSString stringWithUTF8String:oKey];
return [kv getBoolForKey:key defaultValue:defaultValue];
}
retu
4、Lib源码分析
打开lib文件夹后,我们只看到一个文件就是mmkv.dart
其中的原理是使用dart.ffi库
final DynamicLibrary _nativeLib = Platform.isAndroid
? DynamicLibrary.open("libmmkv.so")
: DynamicLibrary.process();
然后通过加载原生库后,寻找其对用的函数指针地址,转换为dart的函数
final int Function(Pointer<Void>, Pointer<Utf8>, int) _encodeBool = _nativeLib
.lookup<NativeFunction<Int8 Function(Pointer<Void>, Pointer<Utf8>, Int8)>>(
_nativeFuncName("encodeBool"))
.asFunction();
final int Function(Pointer<Void>, Pointer<Utf8>, int) _decodeBool = _nativeLib
.lookup<NativeFunction<Int8 Function(Pointer<Void>, Pointer<Utf8>, Int8)>>(
_nativeFuncName("decodeBool"))
.asFunction();
然后调用,调用的时候有类型转账,需要注意转换String类型到ffi的Pointer,最后使用后需要释放calloc.free
bool encodeBool(String key, bool value) {
var keyPtr = key.toNativeUtf8();
var ret = _encodeBool(_handle, keyPtr, _bool2Int(value));
calloc.free(keyPtr);
return _int2Bool(ret);
}
bool decodeBool(String key, {bool defaultValue = false}) {
var keyPtr = key.toNativeUtf8();
var ret = _decodeBool(_handle, keyPtr, _bool2Int(defaultValue));
calloc.free(keyPtr);
return _int2Bool(ret);
}
| Dart type | Description |
|---|---|
| Bool | Represents a native bool in C. |
| Double | Represents a native 64 bit double in C. |
| Float | Represents a native 32 bit float in C. |
| Int8 | Represents a native signed 8 bit integer in C. |
| Int16 | Represents a native signed 16 bit integer in C. |
| Int32 | Represents a native signed 32 bit integer in C. |
| Int64 | Represents a native signed 64 bit integer in C. |
| NativeFunction | Represents a function type in C. |
| Opaque | The supertype of all opaque types in C. |
| Uint8 | Represents a native unsigned 8 bit integer in C. |
| Uint16 | Represents a native unsigned 16 bit integer in C. |
| Uint32 | Represents a native unsigned 32 bit integer in C. |
| Uint64 | Represents a native unsigned 64 bit integer in C. |
| Void | Represents the void type in C. |
具体细节可以查看官网文档C interop using dart:ffi | Dart
对于某些数据函数的处理其中还涉及到内存的申请和释放,通过指针获取到String数据
/// Encode an utf-8 string.
bool encodeString(String key, String? value) {
if (value == null) {
removeValue(key);
return true;
}
final keyPtr = key.toNativeUtf8();
final bytes = MMBuffer.fromList(Utf8Encoder().convert(value))!;
var ret = _encodeBytes(_handle, keyPtr, bytes.pointer!, bytes.length);
calloc.free(keyPtr);
bytes.destroy();
return _int2Bool(ret);
}
/// Decode as an utf-8 string.
String? decodeString(String key) {
var keyPtr = key.toNativeUtf8();
final lengthPtr = calloc<Uint64>();
var ret = _decodeBytes(_handle, keyPtr, lengthPtr);
calloc.free(keyPtr);
if (ret != nullptr) {
var length = lengthPtr.value;
calloc.free(lengthPtr);
var result = _buffer2String(ret, length);
if (!Platform.isIOS && length > 0) {
calloc.free(ret);
}
return result;
}
calloc.free(lengthPtr);
return null;
}
我们着重说一下:
- 申请指定大小的内存,并得到指向改地址的指针
Pointer<Uint8>? _ptr;
const Allocator malloc = _MallocAllocator();
_ptr = malloc<Uint8>(length);
- 申请指向改NULL地址的指针
Pointer<Uint8>? _ptr;
final Pointer<Never> nullptr = Pointer.fromAddress(0);
_ptr = nullptr;
- 申请一个指针地址备用,可用于存储int类型
const Allocator calloc = _CallocAllocator();
final lengthPtr = calloc<Uint64>();
- String转指针
Pointer<Utf8> _string2Pointer(String? str) {
if (str != null) {
return str.toNativeUtf8();
}
return nullptr;
}
- 指针转String
String? _pointer2String(Pointer<Utf8>? ptr) {
if (ptr != null && ptr != nullptr) {
return ptr.toDartString();
}
return null;
}
- 指定长度的Buffer转String
String? _buffer2String(Pointer<Uint8>? ptr, int length) {
if (ptr != null && ptr != nullptr) {
var listView = ptr.asTypedList(length);
return Utf8Decoder().convert(listView);
}
return null;
}
- String 转Buffer
static xx bufferFromString(String value) {
List<int> list = Utf8Encoder().convert(value)//先转换String
Pointer<Uint8>? _ptr;//buffer指针地址
int _length = 0;//buffer长度
_length = list.length;
if (_length == 0) {
buffer._ptr = malloc<Uint8>();
}
buffer.asList()!.setAll(0, list);
return buffer;
}
ptr.asTypedList(length)的补充说明:
external Uint8List asTypedList(int length)
在地址空间中创建由内存支持的类型化列表视图。
返回的视图将允许访问从address到address + length的内存范围。
5、总结
- Flutter上的MKKV的本质依然是使用原生的
Android和iOS上的MKKV - iOS上对MKKV本来的函数再次进行了函数封装,同步了使用名称,同时转换为
C函数使dart.ffi可以发现 - Flutter上的MKKV不走
MethodChannel通道来使用key-value,而是为了性能使用ffi通道 - Flutter的MKKV使用数据存储或者数据获取 函数需要多次调用
ffi通道来转换C和Dart之间的数据,涉及到内存的开辟和释放