Flutter Plugin 使用Protobuf 协议

1,866 阅读4分钟

Protobuf 简单介绍

Protobuf是google推出的数据传输协议,对比json,相同数据量,protobuf储存空间可以压缩的json的1/3左右。并且解析速度还比json快。各位平台支持的也挺好的,为什么不用! 如果想更进一步了解,可以参考官网

使用背景

我们知道,使用FlutterChannel传递大量大块数据,因为内存拷贝和类型转化,且需要在Platform线程(插件代码运行的线程;即Android/iOS的主线程)中传递,这样就会导致UI卡顿。所以我们想可以使用Protobuf格式二进制数据来替代普通数据(字符串,整形等)进行Flutter和Navtive通信。这样理论上传输的内存块会降低2/3。还是值得研究使用的。

使用方式

1. 创建Flutter项目

我们已新建flutter plugin项目为例。当然Flutter Application项目一样可以使用,使用方式一样。

2. 定义协议文件 data_channel.proto

这个协议文件就是需要传递的数据,使用方式很简单。注意属性序号不能有重复哦。

syntax = "proto3";

// `java_package` should match the package name you declare for `androidPackage` in your pubspec.yaml
option java_package = "com.example.flutter_channel_sample";
option java_outer_classname = "Leaf";
option objc_class_prefix = "Leaf";

message DataChannel {
    string path = 1;
    repeated string sqlList = 2;
}

3. 把协议文件生成dart文件

进入协议文件所在目录,执行以下命令。当然需要提前安装编译器protoc

 protoc --dart_out=./  data_channel.proto

生成dart文件如下,主要关注 data_channel.pb.dart 文件。

image.png

protoc macos 安装方式

$ brew install protobuf

$ protoc --version # Ensure compiler version is 3+

其他平台可参考

生成之后,其实就成了Dart里面的对象,实例化之后就可以了。

import 'package:flutter_channel_sample/gen/protos/data_channel.pb.dart';


DataChannel c = DataChannel();
c.path = "test";
c.sqlList.add("select * frome user");
final jonsStr = c.writeToJsonMap();
final data = c.writeToBuffer();

现在考虑的就是如何把protobuf协议实例传递到原生

基本知识储备,先说简单说明一下。

Flutter定义了三种不同类型的Channel,用来和原生传递消息数据。它们分别是。

  • MethodChannel:用于传递方法调用(method invocation)。
  • BasicMessageChannel:用于传递字符串和半结构化的信息。
  • EventChannel: 用于数据流(event streams)的通信。

三种Channel之间互相独立,各有用途,但它们在设计上却非常相近。每种Channel均有三个重要成员变量:

  • name: String类型,代表Channel的名字,也是其唯一标识符。
  • messager:BinaryMessenger类型,代表消息信使,是消息的发送与接收的工具。
  • codec: MessageCodec类型或MethodCodec类型,代表消息的编解码器。后文我们有自定义解码器。

一、使用MethodChannel传递数据

1. 创建_channel实例

static const MethodChannel _channel = MethodChannel('flutter_channel_sample');

2. 定义接口

大家看参数传递第是Uint8List 二进制类型,因为默认standard messsage codec的解码器不支持Protobuf数据格式。

Flutter官方文档表示,standard platform channels使用standard messsage codecmessageresponse进行序列化和反序列化,messageresponse可以是booleansnumbersStringsbyte buffers, ListMaps等等,而序列化后得到的则是二进制格式的数据。

static Future<String?> batchProtoBufToNavtive(Uint8List bufData) async {
  try {
    final String? rep = await _channel
        .invokeMethod("batchProtoBufToNavtive", {'data': bufData});
    return rep;
  } catch (e) {
    debugPrint("batchStringFoNavtive, $e");
    rethrow;
  }
}

到此,dart端的代码就完成了!接下来且看iOS端如何实现。

3. 在flutter_channel_sample.podspec 文件中添加依赖 s.dependency 'Protobuf', '3.21.2'

结构如下: 这个就再iOS项目中添加了Protobuf支持。

Pod::Spec.new do |s|
  s.name             = 'flutter_channel_sample'
  s.version          = '0.0.1'
  s.summary          = 'A new Flutter project.'
  s.description      = <<-DESC
A new Flutter project.
                       DESC
  s.homepage         = 'http://example.com'
  s.license          = { :file => '../LICENSE' }
  s.author           = { 'Your Company' => 'email@example.com' }
  s.source           = { :path => '.' }
  s.source_files = 'Classes/**/*'
  s.public_header_files = 'Classes/**/*.h'
  s.dependency 'Flutter'
  s.dependency 'Protobuf', '3.21.2'
  s.platform = :ios, '8.0'

  # Flutter.framework does not contain a i386 slice.
  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
end

4. 通过protobuf协议文件,生成iOS源码

 protoc *.proto --objc_out=./ 

生成之后,把源码添加到xcode项目中如下图。这样,iOS端的准备工作就完成了。接下来就是实现Flutter Plugin接口。

image.png

5. iOS端实现接口

if ([@"batchProtoBufToNavtive" isEqualToString:call.method]) {

      NSError *err = nil;

      FlutterStandardTypedData *fx = call.arguments[@"data"];

      LeafDataChannel *xx = [LeafDataChannel parseFromData:fx.data error:&err];

      NSLog(@"batchStringFoNavtive path: %@", xx.path);

      NSLog(@"batchStringFoNavtive strList Count: %ld -- %@", xx.sqlListArray_Count ,xx.sqlListArray.firstObject);

      result(@"done!");

  }

完成之后,变可以执行iOS项目查看结果。

二、使用BasicMessageChannel传递数据

Platform Channel实际上是支持大内存数据块的传递,当需要传递大内存数据块时,需要使用BasicMessageChannel以及BinaryCodec。而整个数据传递的过程中,唯一可能出现数据拷贝的位置为native二进制数据转化为Dart语言二进制数据。若二进制数据大于阈值时(目前阈值为1000byte)则不会拷贝数据,直接转化,否则拷贝一份再转化。

1. 定义_basicChannel

其中的LeafTestCodec(),是自定义的解码器,用来把protobuf生成的实例序列号和反序列号成二进制文件。 系统其实也提供了几个默认解码器。比如二进制可以使用BinaryCodec, json可以使用JSONMessageCodec。文末的源码链接中,可以查看到项目中使用了JSONMessageCodec解码器。

image.png

  static const BasicMessageChannel _basicChannel =
      BasicMessageChannel<DataChannel>(
          "flutter_channel_sample.data", LeafTestCodec());

2. 自定义编码和解码器 leaf_test_codec.dart

自定义解码器,需要实现MessageCodec<T>协议的decodeMessageencodeMessage方法就行。具体可以阅读如下代码。

class LeafTestCodec implements MessageCodec<DataChannel> {
  /// Creates a [MessageCodec] with UTF-8 encoded String messages.
  const LeafTestCodec();

  @override
  DataChannel? decodeMessage(ByteData? message) {
    if (message == null) return null;
    final d = message.buffer
        .asUint8List(message.offsetInBytes, message.lengthInBytes);
    return DataChannel.fromBuffer(d);
  }

  @override
  ByteData? encodeMessage(DataChannel? message) {
    if (message == null) return null;
    final Uint8List encoded = message.writeToBuffer();
    return encoded.buffer.asByteData();
  }
}

3. 定义插件接口

现在大家看这个接口的参数使用的Protobuf协议文件生成的DataChannel对象。因为我们自定义了解码编码器,就可以这样使用了。

static Future<DataChannel?> sendBinaryData(DataChannel byteData) async {
  try {
    final DataChannel? rep = await _basicChannel.send(byteData);
    return rep;
  } catch (e) {
    debugPrint("batchStringFoNavtive, $e");
    rethrow;
  }
}

4. 使用!当然使用之前,还需要再原生端实现改插件接口。且先看下一步。

void _binaryPassData() async {
  final s = DateTime.now();

  final rep = await FlutterChannelSample.sendBinaryData(bufDataChannel!);

  print('使用时长: ${DateTime.now().difference(s).inMilliseconds} 毫秒');
  print("返回结果: $rep");
}

5. iOS实现 BasicMessageChannel 接口。

眼尖的读者应该发现了,iOS端拿到的message消息实例,是用Protobuf协议生成的iOS对象实例。而不需要做类型转换,这得益于我们自定义的解码器暗中帮助!是不是非常nice!

FlutterBasicMessageChannel* myTestChannel  = [[FlutterBasicMessageChannel alloc] initWithName:@"flutter_channel_sample.data" binaryMessenger:[registrar messenger] codec:[TestCodec sharedInstance]];
[myTestChannel setMessageHandler:^(id  _Nullable message, FlutterReply  _Nonnull callback) {
    NSLog(@"接收到flutter 数据");
    LeafDataChannel *model = message;
    NSLog(@"接收到flutter 数据: %@", model.path);
    
    LeafDataChannel *back = [[LeafDataChannel alloc] init];
    back.path = @"back";
    callback(back);
}];

小结

通过测试发现,使用BasicMessageChannel传递大块数据的确比MethodChannel要快些,因为少了一次内存拷贝。当数据量超高50W条(每条150个字符串)的时候,就开始体现优势。

通过阅读本文,可以从实战学会如何创建一个Flutter Plugin项目。文中包含了常使用的MethodChannel,还有自定义解码便器来传递消息和数据。

本文已Flutter和iOS为例实现,对于Android端,其实思路一样,有空补上。

源码传送门