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 文件。
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 codec
对message
和response
进行序列化和反序列化,message
与response
可以是booleans
,numbers
,Strings
,byte buffers
,List
,Maps
等等,而序列化后得到的则是二进制格式的数据。
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接口。
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解码器。
static const BasicMessageChannel _basicChannel =
BasicMessageChannel<DataChannel>(
"flutter_channel_sample.data", LeafTestCodec());
2. 自定义编码和解码器 leaf_test_codec.dart
自定义解码器,需要实现MessageCodec<T>
协议的decodeMessage
和encodeMessage
方法就行。具体可以阅读如下代码。
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端,其实思路一样,有空补上。