本文为 Flutter 与 Native 通信详解系列文章的下篇,在阅读本篇文章前,建议先看一下上篇文章:Flutter 与 Native 通信详解(上):原理探究 https://www.jianshu.com/p/fafc6b3f3aa1
在上一篇中我们已经探究了Flutter与Native通信的原理:通过消息信使(BinaryMessenger)来异步的收发二进制消息,每个消息都有对应的消息渠道(channel)来区分不同的消息用途,然后使用不同的消息编解码器(Codec)对二进制数据进行序列化与反序列化,最后通过注册的消息处理器(MessageHandler)来处理并回复对应的消息。
其中有四个要素:消息信使(BinaryMessenger),消息渠道(channel),消息处理器(MessageHandler),消息编解码器(Codec). 在有了这4个要素之后,我们其实已经能够在Flutter和native端收发消息了 (其实有前3个要素就够了,codec是为了能够方便收发更复杂的消息),但是直接用这四要素来收发消息有点麻烦,每当要发送消息或注册handler时,需要指定正确的 channel,还需要我们自己调用 Codec 来序列化和反序列化消息,所以 Flutter 提供一个工具: Platform Channel,让我们能够更方便的与平台端通信。
Platform Channel 是所以 Flutter 提供的和平台端通信的工具,针对不同的使用场景,Platform Channel 又分为以下三类(Platform Channel 其实是以下三类的统称):
Message channel:用于传递字符串和半结构化的信息Method channel:用于传递方法调用(method invocation)Event channel:用于数据流(event streams)的通信观察各平台三种 Platform Channel 的源码实现,发现其都会包含以下3个成员变量:
name: String类型,即 channel name,用来唯一标识一个Platform Channelmessager:BinaryMessenger类型,代表消息信使,是消息的发送与接收的工具。codec: MessageCodec类型或MethodCodec类型(底层还是基于MessageCodec),代表消息的编解码器。而四元素中的消息处理器(MessageHandler)则存储于一个全局的hashmap中,key为其对应的 channel name.
聪明的同学可能已经猜到:Platform Channel 其实就是对前面说的通信四要素的整合封装。
MessageChannel 是 Platform Channel 中最基本的一种,用于字符串或者半结构化消息(Codec支持的格式)的收发,在 Dart 和 Android 端实现为 BasicMessageChannel 类,在 iOS 端实现为 FlutterBasicMessageChannel 类。
为什么前面要加个“basic”呢? 这里有个小插曲,在最开始时 Platform Channel 只有 MessageChannel 这一种(那时还没有basic前缀),但是后来又出现了 MethodChannel ,且 MethodChannel 被使用的更普遍,为了避免和 MethodChannel 名称混淆,又因为 MessageChannel 作用比较单一,所以在前面加了个”basic“前缀。
先来看一个例子:
// Flutter 端发送字符串消息 // 创建channel const channel = BasicMessageChannel<String>('foo', StringCodec()); // 发送 final String reply = await channel.send('Hello from Flutter!'); print(reply); // 接收 channel.setMessageHandler((String message) async { print('Received: $message'); return 'Reply form Flutter!'; }); // Android 端 // 创建channel val channel = BasicMessageChannel<String>( flutterView, "foo", StringCodec.INSTANCE) // 发送 channel.send("Hello from Android!") { reply -> Log.i("MSG", reply) } // 接收 channel.setMessageHandler { message, reply -> Log.i("MSG", "Received: $message") reply.reply("Reply from Android") } // iOS端 // 创建channel FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"foo" binaryMessenger:controller codec:[FlutterStringCodec sharedInstance]]; // 发送 [channel sendMessage:@"Hello from iOS!" reply:^(id _Nullable reply) { NSLog(@"%@", reply); }]; // 接收 [channel setMessageHandler:^(id _Nullable message, FlutterReply _Nonnull callback) { NSLog(@"Received: %@", message); callback(@"Reply from iOS!"); }];上面的代码演示了利用 MessageChannel 在 Flutter和平台端(iOS/Android)互相收发消息的过程。
相比上篇文章中使用的 BinaryMessenger 可以说是方便了不少:channel 名只需在创建 channel的时候指定, 之后待channel对象即可方便的进行收发消息的操作, 而且使用了 StringCodec 来对二进制数据进行序列化和反序列化(这里针对收发数据的不同,可以使用上篇文章中说到的4种类型的 Codec)。
前面说到,Platform Channel 其实就是对通信四要素的整合封装,MessageChannel 本身做的工作不是很多,大部分的工作都是交由底层来完成的:
MessageChannel 的消息发送其实还是通过 BinaryMessenger 来实现的使用了 Codec 来对消息进行序列化和反序列化,消息最终还是需要转换为二进制数据进行传递MessageChannel 是轻量级的,无状态的,它不会记录调用其注册方法注册的消息 handler两个 channel 名相同并且 Codec 类型也相同的 MessageChannel 是等价的,它们如果同时进行消息收发会互相干扰上面 MessageChannel 的例子,其实等价于下面用 BinaryMessenger 的实现:
// Flutter 端 const codec = StringCodec(); // 发送消息 final String reply = codec.decodeMessage( await BinaryMessages.send( 'foo', codec.encodeMessage('Hello from Flutter!'), ), ); print(reply); // 接收消息 BinaryMessages.setMessageHandler('foo', (ByteData message) async { print('Received: ${codec.decodeMessage(message)}'); return codec.encodeMessage('Reply form Flutter!'); });Method channel 的作用是调用 Flutter 端或者平台端的方法或者代码段,也是平时开发中最常用的一种 channel,实际上它也是按方法调用的逻辑来设计的。
首先,调用方法需要指定方法名和需要传递的参数,而 MethodChannel 提供了一个类 MethodCall 用来表示方法调用,其内部就包含了方法名和参数:
// message_codec.dart /// An command object representing the invocation of a named method. class MethodCall { /// Creates a [MethodCall] representing the invocation of [method] with the /// specified [arguments]. const MethodCall(this.method, [this.arguments]) : assert(method != null); /// The name of the method to be called. final String method; /// The arguments for the method. /// /// Must be a valid value for the [MethodCodec] used. final dynamic arguments; @override String toString() => '$runtimeType($method, $arguments)'; }其次,调用方法都会有返回值,Flutter 抽象了一个叫做信封(envelope)的概念用来表示 MethodChannel 的返回值,返回值分为成功和失败两种,成功时会返回对应的返回值内容(可能为null),失败时则返回一个 error,error 包括一个 String 类型的 code, 一个 String 类型的 message,和一个自定义类型的 detail (受编解码器支持类型的约束),通常为 null.
最后,Flutter 提供了一种专门的编解码器:MethodCodec,用来支持 MethodChannel 的这种调用方式,MethodCodec 分为以下两类:
JSONMethodCodec:JSONMethodCodec的编解码依赖于JSONMessageCodec,当其在编码MethodCall时,会先将MethodCall转化为字典{"method":method,"args":args}。其在编码返回值时,会将其转化为一个数组,调用成功为[result],调用失败为[code,message,detail]。再使用JSONMessageCodec将字典或数组转化为二进制数据。
StandardMethodCodec:MethodCodec的默认实现,StandardMethodCodec的编解码依赖于StandardMessageCodec,当其编码MethodCall时,会将method和args依次使用StandardMessageCodec编码,写入二进制数据容器。其在编码方法的返回值时,若调用成功,会先向二进制数据容器写入数值0(代表调用成功),再写入StandardMessageCodec编码后的result。而调用失败,则先向容器写入数据1(代表调用失败),再依次写入StandardMessageCodec编码后的code,message和detail。
让我们看一个完整的 MethodChannel 使用示例(包含错误处理):
// Flutter 端. const channel = MethodChannel('foo'); // 调用平台端的方法,调用 baz 将发生错误 const name = 'bar'; // or 'baz', or 'unknown' const value = 'world'; try { print(await channel.invokeMethod(name, value)); } on PlatformException catch(e) { print('$name failed: ${e.message}'); } on MissingPluginException { print('$name not implemented'); } // 接收来自平台端的方法调用并返回结果 channel.setMethodCallHandler((MethodCall call) async { switch (call.method) { case 'bar': return 'Hello, ${call.arguments}'; case 'baz': throw PlatformException(code: '400', message: 'This is bad'); default: throw MissingPluginException(); } }); // Android端 val channel = MethodChannel(flutterView, "foo") // 调用 Flutter 端的方法,调用 baz 将发生错误 val name = "bar" // or "baz", or "unknown" val value = "world" channel.invokeMethod(name, value, object: MethodChannel.Result { override fun success(result: Any?) { Log.i("MSG", "$result") } override fun error(code: String?, msg: String?, details: Any?) { Log.e("MSG", "$name failed: $msg") } override fun notImplemented() { Log.e("MSG", "$name not implemented") } }) // 接收来自 Flutter端的方法调用并返回结果 channel.setMethodCallHandler { call, result -> when (call.method) { "bar" -> result.success("Hello, ${call.arguments}") "baz" -> result.error("400", "This is bad", null) else -> result.notImplemented() } } // iOS端 FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"foo" binaryMessenger:controller]; NSString *name = @"bar"; // or "baz", or "unknown" NSString *value = @"world"; // 调用 Flutter 端的方法,调用 baz 将发生错误 [channel invokeMethod:name arguments:value result:^(id _Nullable result) { if ([result isKindOfClass:[FlutterError class]]) { // 调用出错 FlutterError *resultError = (FlutterError *)result; NSLog(@"%@ failed: %@", name, resultError.message); } else if ([FlutterMethodNotImplemented isEqual:result]) { // 调用的方法未实现 NSLog(@"%@ not implemented", name); } else { // 调用成功 NSLog(@"%@", result); } }]; // 接收来自 Flutter端的方法调用并返回结果 [channel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) { if ([call.method isEqualToString:@"bar"]) { result([NSString stringWithFormat:@"Hello, %@", call.arguments]); } else if ([call.method isEqualToString:@"baz"]) { result([FlutterError errorWithCode:@"400" message:@"This is bad" details:nil]); } else { result(FlutterMethodNotImplemented); } }];示例代码演示了在三端分别使用 MethodChannel 进行消息收发的过程,并对过程中产生的异常情况(方法调用出错、方法未实现)进行了处理。
EventChannel 是一个单向的通道,目前只能由 Native 端向 Flutter 端发送消息,而这种发送与前面的 MethodChannel 不同,EventChannel 的消息传递形式更像一种 Push,通常用于平台端向 Flutter push一些设备状态信息,比如,当前网络质量,设备充电状态等,当然也可以通过轮询的方式定时去获取这些设备状态,但是相比之下,当平台端检测到充电状态发生改变时,主动向 Flutter 端 Push 充电状态信息会更高效。
看个例子:
// Flutter 端 // 创建 EventChannel static const EventChannel eventChannel = EventChannel('samples.flutter.io/charging'); // 设置事件监听方法 eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError); // 成功事件回调 void _onEvent(Object event) { setState(() { _chargingStatus = "Battery status: ${event == 'charging' ? '' : 'dis'}charging."; }); } // 失败错误回调 void _onError(Object error) { setState(() { _chargingStatus = 'Battery status: unknown.'; }); } // iOS 端 FlutterEventSink _eventSink; // 成员变量 // 创建 EventChannel FlutterEventChannel* chargingChannel = [FlutterEventChannel eventChannelWithName:@"samples.flutter.io/charging" binaryMessenger:controller]; // 设置 StreamHandler [chargingChannel setStreamHandler:self]; // FlutterStreamHandler 协议方法 - (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { // 记录 eventSink(事件发射器) _eventSink = eventSink; // 开启电池状态监控 [[UIDevice currentDevice] setBatteryMonitoringEnabled:YES]; // 发送首次事件 [self sendBatteryStateEvent]; // 监听电池状态变化通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onBatteryStateDidChange:) name:UIDeviceBatteryStateDidChangeNotification object:nil]; return nil; } - (FlutterError*)onCancelWithArguments:(id)arguments { [[NSNotificationCenter defaultCenter] removeObserver:self]; // 清空当前记录的 eventSink _eventSink = nil; // 关闭电池状态监控 [[UIDevice currentDevice] setBatteryMonitoringEnabled:NO]; return nil; } // 电池状态变化通知 - (void)onBatteryStateDidChange:(NSNotification*)notification { [self sendBatteryStateEvent]; } // 发送电池状态事件 - (void)sendBatteryStateEvent { if (!_eventSink) return; UIDeviceBatteryState state = [[UIDevice currentDevice] batteryState]; switch (state) { case UIDeviceBatteryStateFull: case UIDeviceBatteryStateCharging: _eventSink(@"charging"); break; case UIDeviceBatteryStateUnplugged: _eventSink(@"discharging"); break; default: _eventSink([FlutterError errorWithCode:@"UNAVAILABLE" message:@"Charging status unavailable" details:nil]); break; } }示例代码为官方提供的 demo,完整代码和 Android 端 Demo 见:platform channel example
EventChannel 的使用与之前的 Channels 不太一样,它具备响应式编程的特点,将平台端发送的数据抽象为事件流(EventStream),然后 Flutter 端通过监听(listen)来获取事件流中的数据并进行处理:Flutter 端需要先设置事件监听方法,在监听方法中处理平台端push过来的消息或error;平台端需要实现一个 StreamHandler(iOS端是protocol,Android端是interface),需要实现其 onListen 和 onCancel 方法。而在onListen方法的入参中,有一个EventSink(其在Android是一个对象,iOS端则是一个block)。我们持有EventSink后,即可通过 EventSink 向Flutter端发送事件消息,例子中则是监听了设备的充电状态,在充电状态发送改变时,使用 EventSink 将当前充电状态 push 给了 Flutter 端。
如前面所说,EventChannel 其实也是整合封装了通信四要素。当我们注册了一个 StreamHandler 后,实际上会注册一个对应的BinaryMessageHandler 到 BinaryMessager。而当 Flutter 端开始监听事件时,会发送一个二进制消息到平台端。平台端用 MethodCodec 将该消息解码为 MethodCall,如果 MethodCall 的 method 的值为 "listen",则调用 StreamHandler 的 onListen 方法,传递给StreamHandler 一个 EventSink。而通过 EventSink 向 Flutter 端发送消息时,实际上就是通过 BinaryMessager 的 send 方法将消息传递过去,如果 MethodCall 的 method 的值为 "cancel",则调用 StreamHandler 的 onCancel 方法。
onListen方法调用时机为 Flutter 端第一个对应 channel 的监听器被注册时,onCancel 方法的调用时机为 Flutter 端对应 channel 的最后一个监听器被取消时,这也代表着 EventChannel 一次监听生命周期的结束,所以不要忘了在 onCancel 方法中做一些清理工作。
平台端也可以主动结束事件流,使用 EventSink 发送 endOfStream(iOS端将 FlutterEndOfEventStream 常量传入EventSink Block,Android 则直接调用 EventSink 的e ndOfStream 方法) 消息,即可将事件流关闭。
Flutter 与 Native 通信在 Flutter 应用中几乎是不可缺少的步骤,所以掌握其原理和实际用法尤其重要。Flutter 与 Native 通信详解系列上下两篇文章,先从底层视角切入,探究了 Flutter 与 Native 通信的基础形态和原理,然后说明了上层的 Platform Channel 其实是底层通信四要素的整合封装,再过渡到实际应用,分别介绍了三种 Platform Channel 在各端的具体用法与特点,希望能帮助大家更好的理解与掌握 Flutter 与 Native 通信的相关技术。
Flutter Platform Channels深入理解Flutter Platform Channelflutter engine 源码Writing custom platform-specific code
作者:我叫纠结伦 链接:https://www.jianshu.com/p/a467f02fb5ad 来源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
