第三方库源码分析第二篇 * Flutter高性能通用 key-value 组件 MMKV

1,672 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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 typeDescription
BoolRepresents a native bool in C.
DoubleRepresents a native 64 bit double in C.
FloatRepresents a native 32 bit float in C.
Int8Represents a native signed 8 bit integer in C.
Int16Represents a native signed 16 bit integer in C.
Int32Represents a native signed 32 bit integer in C.
Int64Represents a native signed 64 bit integer in C.
NativeFunctionRepresents a function type in C.
OpaqueThe supertype of all opaque types in C.
Uint8Represents a native unsigned 8 bit integer in C.
Uint16Represents a native unsigned 16 bit integer in C.
Uint32Represents a native unsigned 32 bit integer in C.
Uint64Represents a native unsigned 64 bit integer in C.
VoidRepresents 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;
  }

我们着重说一下:

  1. 申请指定大小的内存,并得到指向改地址的指针
Pointer<Uint8>? _ptr;
const Allocator malloc = _MallocAllocator();
_ptr = malloc<Uint8>(length);
  1. 申请指向改NULL地址的指针
Pointer<Uint8>? _ptr;      
final Pointer<Never> nullptr = Pointer.fromAddress(0);
_ptr = nullptr;
  1. 申请一个指针地址备用,可用于存储int类型
const Allocator calloc = _CallocAllocator();
final lengthPtr = calloc<Uint64>();
  1. String转指针
Pointer<Utf8> _string2Pointer(String? str) {
  if (str != null) {
    return str.toNativeUtf8();
  }
  return nullptr;
}

  1. 指针转String
String? _pointer2String(Pointer<Utf8>? ptr) {
  if (ptr != null && ptr != nullptr) {
    return ptr.toDartString();
  }
  return null;
}
  1. 指定长度的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;
}
  1. 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、总结

  1. Flutter上的MKKV的本质依然是使用原生的AndroidiOS上的MKKV
  2. iOS上对MKKV本来的函数再次进行了函数封装,同步了使用名称,同时转换为C函数使dart.ffi可以发现
  3. Flutter上的MKKV不走MethodChannel通道来使用key-value,而是为了性能使用ffi通道
  4. Flutter的MKKV使用数据存储或者数据获取 函数需要多次调用ffi通道来转换CDart之间的数据,涉及到内存的开辟和释放