Flutter 简介
Flutter 是 Google 开发的一套全新的跨平台框架,不同于 React Native 封装原生应用层接口,然后通过 JavaScriptCore 转义 JavaScript 来生成原生界面的方案,Flutter 抛开原生控件,用 Dart 语言重写了一套跨平台的UI组件(widget),渲染引擎依靠跨平台的 Skia 图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,前段时间 Google I/O 2019 大会上宣布 Flutter 还将支持 Web端、桌面端和嵌入式设备,可谓真正实现了 Write once, run anywhere
.
Flutter 和 Native 通信的必要性
看起来 Flutter 做了很多,似乎完全不需要使用原生平台的功能也能构建一个 App,但是大部分情况下,光靠 Flutter 是不够的,比如当你的App需要如下功能时:
- 获取设备信息,像电池电量、网络连接状态等
- 使用相机,麦克风,蓝牙,定位等功能
- 数据持久化,通知,App 生命周期等
当然,这些平台(即Native端,下同)相关的功能 Flutter 其实也可以封装在 Flutter 框架中,让开发者不用关心平台,完全通过Flutter 构建App,多好啊;但是这样做会带来一些问题,且不论将Android、iOS(之后还会包括Web端、桌面端、嵌入式设备)各端的平台功能都封装好的难易度,就说如果都封装到 Flutter 中了,那么Flutter framework 变得比现在大很多,且只要平台相关API有所更新,Flutter 也得跟着修改,会出现 Flutter 一直在追逐平台版本的情况,也容易出现兼容性问题和版本碎片化.
所以,Flutter 团队并没有选择封装平台相关的API,选择了另一种更灵活的做法:Flutter 依旧用 Dart 及跨平台的渲染引擎来实现 Write once, run anywhere
的界面和业务逻辑,在涉及到平台相关的功能时,则还是由开发者在平台端实现,然后提供了一个叫做 Platform Channel 的机制来进行 Flutter 和平台端之间的通信,这样做可以将Flutter和平台的耦合度降到最低。
Platform Channel 见名知意,即平台通道,是Flutter和原生平台通信的通道,分为以下三类:
- Message channel:用于传递字符串和半结构化的信息。
- Method channel:用于传递方法调用(method invocation)。
- Event channel:用于数据流(event streams)的通信。
本篇文章先不着急介绍这三类 Platform channel,先来看看 Flutter 和平台端通信的原理。
Flutter 和 Native 通信的原理
消息信使:BinaryMessenger
从底层来看,Flutter和平台端通信的方式是发送异步的二进制消息,该基础通信方式在Flutter端由BinaryMessages
来实现, 而在Android端是一个接口BinaryMessenger
,其具体实现为FlutterNativeView
,在iOS端是一个协议 FlutterBinaryMessenger
,FlutterViewController
遵守并实现了这个协议。
其主要实现了发送二进制消息和设置消息处理回调的方法,如Flutter端BinaryMessages 的部分源码:
// 发送二进制消息
static Future<ByteData> send(String channel, ByteData message) {
final _MessageHandler handler = _mockHandlers[channel];
if (handler != null)
return handler(message);
return _sendPlatformMessage(channel, message);
}
// 注册消息处理回调
static void setMessageHandler(String channel, Future<ByteData> handler(ByteData message)) {
if (handler == null)
_handlers.remove(channel);
else
_handlers[channel] = handler;
}
消息通道:Channel
同时,为了区分不同用途的消息,每个消息都可以为其指定一个channel
,即以上消息收发方法中的参数 channel,channel 仅仅是一个字符串,下面的例子使用foo
当做消息收发的channel:
//向平台发送二进制消息.
final WriteBuffer buffer = WriteBuffer()
..putFloat64(3.1415)
..putInt32(12345678);
final ByteData message = buffer.done();
await BinaryMessages.send('foo', message);
print('Message sent, reply ignored');
Android 端(Kotlin):
// 接受并解析来自Flutter端的消息
flutterView.setMessageHandler("foo") { message, reply ->
message.order(ByteOrder.nativeOrder())
val x = message.double
val n = message.int
Log.i("MSG", "Received: $x and $n")
reply.reply(null)
}
iOS端(OC):
// 接受并解析来自Flutter端的消息
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
[controller setMessageHandlerOnChannel:@"foo" binaryMessageHandler:^(NSData * _Nullable message, FlutterBinaryReply _Nonnull reply) {
Float64 x;
NSData *data8 = [message subdataWithRange:NSMakeRange(0, 8)];
[data8 getBytes:&x length:sizeof(x)];
int32_t n;
NSData *data4 = [message subdataWithRange:NSMakeRange(8, 4)];
[data4 getBytes:&n length:sizeof(n)];
NSLog(@"Received %f and %d", x, n);
reply(nil);
}];
消息通信是双向的,所以我们也可以从平台端向Flutter端发送消息,如下:
// 从 Android 端发送一个二进制消息
val message = ByteBuffer.allocateDirect(12)
message.putDouble(3.1415)
message.putInt(123456789)
flutterView.send("foo", message) { _ ->
Log.i("MSG", "Message sent, reply ignored")
}
// 从 iOS端发送一个二进制消息
NSMutableData *message = [NSMutableData dataWithCapacity:12];
Float64 x = 3.1415;
int32_t n = 12345678;
[message appendBytes:&x length:sizeof(x)];
[message appendBytes:&n length:sizeof(n)];
[controller sendOnChannel:@"foo" message:message binaryReply:^(NSData * _Nullable reply) {
NSLog(@"Message sent, reply ignored");
}];
// Flutter端接收
BinaryMessages.setMessageHandler('foo', (ByteData message) async {
final ReadBuffer readBuffer = ReadBuffer(message);
final double x = readBuffer.getFloat64();
final int n = readBuffer.getInt32();
print('Received $x and $n');
return null;
});
消息处理器:MessageHandler
通过上面的示例代码可以发现,消息是通过提前设置的MessageHandler
来处理的,所有的 MessageHandler 都被保存在一个 HashMap 中,key 即为其对应的 channel 字符串,因此每个channel最多只能有一个 MessageHandler,后设置的会将之前的覆盖掉,取消一个 MessageHandler 的方式也就是设置对应 channel 的 MessageHandler 为 null.
在 MessageHandler 中最后的消息回复动作是必须的,每个消息的发送都应该对应一个异步的消息回复,即使没有返回值,也需要回复 null,就像示例代码中一样,这是为了使得Dart中的Future
和平台端的回调函数得以完成和执行。
还有一点需要注意的是,在平台端消息的发送和回复都必须在主线程进行(即UI线程),而在flutter端,每个 Dart isolate 只有一个线程,所以flutter端不用担心用错线程所导致的问题。
关于
Dart isolate
详细讲解可以参考闲鱼团体的文章:Flutter Engine线程管理与Dart Isolate机制
消息编解码器:Codec
在上面的例子中,我们其实已经能够在Flutter和平台间进行相互通信了,但是收发的数据都是二进制的,这就需要开发者考虑更多的细节,如字节顺序(大小端)和怎么表示更高级的消息类型,如字符串,map等,因此,Flutter 还提供了消息编解码器(Codec), 用于高级数据类型(字符串,map等)和二进制数据(byte)之间的转换,即消息的序列化和反序列化。
有了消息编解码器,我们在编程时就不用直接对二进制数据进行操作了,极大的降低了编程复杂度,Flutter 定义了四种基本的消息编解码器类型:
BinaryCodec:BinaryCodec是最为简单的一种Codec,因为其返回值类型和入参的类型相同,均为二进制格式(Android中为ByteBuffer,iOS中为NSData)。实际上,BinaryCodec在编解码过程中什么都没做,只是原封不动将二进制数据消息返回而已。或许你会因此觉得BinaryCodec没有意义,但是在某些情况下它非常有用,比如使用BinaryCodec可以使传递内存数据块时在编解码阶段免于内存拷贝。
StringCodec:使用 UTF-8 编码格式对字符串数据进行编解码,在Android平台转换为 java.util.String 类型,iOS 平台则对应着 NSString.
JSONMessageCodec:JSONMessageCodec用于处理 JSON 数据类型(字符串型,数字型,布尔型,null,只包含这些类型的数组,和key为string类型,value为这些类型的map),在编码过程中,数据会被转换为JSON字符串,然后在使用 UTF-8 格式转换为字节型。其在iOS端使用了NSJSONSerialization作为序列化的工具,而在Android端则使用了其自定义的JSONUtil与StringCodec作为序列化工具。
StandardMessageCodec:StandardMessageCodec 可以认为是 JSONMessageCodec 的升级版,能够处理的数据类型要比 JSONMessageCodec 更普遍一些,且在处理 int 型数据时,会根据 int 数据的大小来转为平台端的32位类型(int)或者是64位类型(long),StandardMessageCodec 也是 Flutter Platform channel 的默认编解码器,下图列出了 StandardMessageCodec 能处理的数据类型和在各平台对应的类型:
Dart | Android | iOS |
---|---|---|
null | null | nil (NSNull when nested) |
bool | java.lang.Boolean | NSNumber numberWithBool: |
int | java.lang.Integer | NSNumber numberWithInt: |
int, if 32 bits not enough | java.lang.Long | NSNumber numberWithLong: |
int, if 64 bits not enough | java.math.BigInteger | FlutterStandardBigInteger (已废弃) |
double | java.lang.Double | NSNumber numberWithDouble: |
String | java.lang.String | NSString |
Uint8List | byte[] | FlutterStandardTypedData typedDataWithBytes: |
Int32List | int[] | FlutterStandardTypedData typedDataWithInt32: |
Int64List | long[] | FlutterStandardTypedData typedDataWithInt64: |
Float64List | double[] | FlutterStandardTypedData typedDataWithFloat64: |
List | java.util.ArrayList | NSArray |
Map | java.util.HashMap | NSDictionary |
需要注意的是 BigInteger 类型在 Dart 2.0 已被废弃,这是因为在Dart 1.0 时,int 类型没有固定的大小限制,32位、64位数字都可以用 int 表示,而当Dart中的int型传到平台端时,就会根据其具体大小转为 int型、long型或者更大的类型,BigInteger 就是用来标识比64位int更大的类型的;但是到了Dart 2.0 ,int 类型大小固定为了 64位,如果想要传递更大的数字,则需要转换为字符串类型。
再深入一点,通过查看 flutter engine源码 可以发现,当message或response需要被编码为二进制数据时,会调用StandardMessageCodec 的 writeValue方法,该方法接收一个名为value的参数,并根据其类型,向二进制数据容器(NSMutableData或ByteArrayOutputStream)写入该类型对应的type值,再将该数据转化为二进制表示,并写入二进制数据容器。
而message或者response需要被解码时,使用的是StandardMessageCodec的readValue方法,该方法接收到二进制格式数据后,会先读取一个byte表示其type,再根据其type将二进制数据转化为对应的数据类型。
假设我们要发送的消息为int型的数字 100,当这个值被转化为二进制数据时,会先向二进制数据容器写入int类型对应的type值:3,再写入由100转化而得的4个byte。而当Flutter端接收到该二进制数据时,先读取第一个byte值,并根据其值得出该数据为int类型,接着读取紧跟其后的4个byte,并将其转化为dart类型的int,反之亦然。
对于字符串、列表、字典的编码会稍微复杂一些。字符串使用UTF-8编码得到的二进制数据是长度不定的,因此会在写入type后,先写入一个代表二进制数据长度的size,再写入数据。列表和字典则是写入type后,先写入一个代表列表或字典中元素个数的size,再递归调用writeValue方法将其元素依次写入。
至于消息编解码器的具体用法,就要说到 Platform channel 了,其实就是对于以上介绍的几个通信基础要素的组合封装,这里由于篇幅问题,下篇文章再说。
总结
到这里,想必你已经理解了flutter和平台端通信的原理:通过消息信使(BinaryMessenger)来异步的收发二进制消息,每个消息都有对应的消息渠道(channel)来区分不同的消息用途,然后使用不同的消息编解码器(Codec)对二进制数据进行序列化与反序列化,最后通过注册的消息处理器(MessageHandler)来处理并回复对应的消息。
参考
Flutter Platform Channels
深入理解Flutter Platform Channel
flutter engine 源码
Writing custom platform-specific code