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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容