Flutter 与 Native 通信详解(下):Platform Channel

本文为 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,让我们能够更方便的与平台端通信。

What is the 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 Channel
  • messager:BinaryMessenger类型,代表消息信使,是消息的发送与接收的工具。
  • codec: MessageCodec类型或MethodCodec类型(底层还是基于MessageCodec),代表消息的编解码器。

而四元素中的消息处理器(MessageHandler)则存储于一个全局的hashmap中,key为其对应的 channel name.

聪明的同学可能已经猜到:Platform Channel 其实就是对前面说的通信四要素的整合封装

Message 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

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 进行消息收发的过程,并对过程中产生的异常情况(方法调用出错、方法未实现)进行了处理。

Event channel

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 Channel
flutter engine 源码
Writing custom platform-specific code

 



作者:我叫纠结伦
链接:https://www.jianshu.com/p/a467f02fb5ad
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值