Flutter之旅:平台通道(Platform Channel)

作为一个UI框架,Flutter提供了三种通道来和原生平台通信。

  • BasicMessageChannel:它提供类似于BinaryMessages的基本消息传递服务,但可自定义消息编解码器,支持发送字符串或半结构化消息。
  • MethodChannel:它使用异步方法调用的方式进行平台通信。
  • EventChannel:它使用事件流的方式进行平台通信。
image.png

三种方式的设计是非常相似的,分别维护了两个成员属性:

  • name:用来标识通道。
  • codec:消息编解码器。

另外,三种方式其实都是通过BinaryMessages来进行消息的传递,它负责将二进制消息发送到平台插件并从平台插件接收二进制消息。最后通过消息编解码器来将二进制信息转换成我们需要的数据类型,注意三种通信方式都是双向的。以BasicMessageChannel为例:

  Future<T> send(T message) async {
    return codec.decodeMessage(await BinaryMessages.send(name, codec.encodeMessage(message)));
  }

细心的同学会发现,图上还有一个OptionalMethodChannel,这并不是一种新的通信方式,而是MethodChannel的进一步封装,如果找不到对应插件,返回的是null,而不再抛出MissingPluginException异常。

class OptionalMethodChannel extends MethodChannel{
  ...
  @override
  Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
    try {
      final T result = await super.invokeMethod<T>(method, arguments);
      return result;
    } on MissingPluginException {
      return null;
    }
  } 
  ...
}

BasicMessageChannel

使用介绍

BasicMessageChannel_Flutter.png
BasicMessageChannel_Android.png

三种通道分别在flutter和其他平台都提供了相关实现,并提供了相似的api,这里以BasicMessageChannel为例展示一下。

成员属性

  • name:通道名字,要保证通信的通道在flutter和native端保持一致。
  • codec:消息的编解码器,同样要保证通信的通道在flutter和native端保持一致。
  • messenger:消息的发送器,类似于flutter中的BinaryMessages。

Api接口

发送消息

flutter端:可以看到发送消息是个异步方法,大概执行顺序就是先将数据编码成字节数据,通过BinaryMessages传输,等待native返回数据,再解码成我们需要的数据,如果native没有返数据,则Future为null。

  Future<T> send(T message) async {
    return codec.decodeMessage(await BinaryMessages.send(name, codec.encodeMessage(message)));
  }

android端:其实实现是类似的,只不过不是使用Future,而是用回调的方式来监听返回数据。

    public void send(T message) {
        this.send(message, (BasicMessageChannel.Reply)null);
    }

    public void send(T message, BasicMessageChannel.Reply<T> callback) {
        this.messenger.send(this.name, this.codec.encodeMessage(message), callback == null ? null : new BasicMessageChannel.IncomingReplyHandler(callback));
    }

接收消息

flutter端:我们可以看到传入的参数是一个异步函数,handler接收的参数为native平台发送的数据,handler的返回值将作为响应返回给native端。

  void setMessageHandler(Future<T> handler(T message)) {
    if (handler == null) {
      BinaryMessages.setMessageHandler(name, null);
    } else {
      BinaryMessages.setMessageHandler(name, (ByteData message) async {
        return codec.encodeMessage(await handler(codec.decodeMessage(message)));
      });
    }
  }

android端:同样是通过接口回调的方式来接收和响应消息的传递。

    public void setMessageHandler(BasicMessageChannel.MessageHandler<T> handler) {
        this.messenger.setMessageHandler(this.name, handler == null ? null : new BasicMessageChannel.IncomingMessageHandler(handler));
    }

    public interface MessageHandler<T> {
        void onMessage(T message, BasicMessageChannel.Reply<T> reply);
    }

示例代码

我们来做这样一个demo,flutter发送一条消息到native,native收到消息后给个回复,并发送一条新的消息到flutter,flutter收到后再回复给native。
flutter:

PluginChannel.listenBasicMessage();
RaisedButton(
            onPressed: () {
              PluginChannel.sendBasicMessage();
            },
            child: Text("BasicMessageChannel"),
          )
...
class PluginChannel {
  static const _basicMessageChannelName = "study_3/basicMessageChannel";
  static const _basicMessageChannel = BasicMessageChannel(_basicMessageChannelName, StandardMessageCodec());

  static void listenBasicMessage(){
    _basicMessageChannel
        .setMessageHandler((result) async{
      print('flutter listen:$result');
      return "flutter response to native";
    });
  }
  static void sendBasicMessage() {
    _basicMessageChannel
        .send("flutter send to native")
        .then((result) {
      print('flutter receive response:$result');
    });
  }
}

android:

    private val BASIC_MESSAGE_CHANNEL = "study_3/basicMessageChannel"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(this)

        basicMessageChanelDemo()
    }
    private fun basicMessageChanelDemo(){
        BasicMessageChannel(this.flutterView,BASIC_MESSAGE_CHANNEL, StandardMessageCodec.INSTANCE)
                .setMessageHandler { any, reply ->
                    println("android listen:$any")
                    reply.reply("android response to flutter")


                    BasicMessageChannel(this.flutterView,BASIC_MESSAGE_CHANNEL, StandardMessageCodec.INSTANCE)
                            .send("android send to flutter"){
                                println("android receive response:$it")
                            }
                }
    }

日志:

I/System.out(27557): android listen:flutter send to native
I/flutter (27557): flutter receive response:android response to flutter
I/flutter (27557): flutter listen:android send to flutter
I/System.out(27557): android receive response:flutter response to native

MethodChannel

一些想法

MethodChannel通过传递方法名和参数,来达到通信的效果,给我的感觉和BasicMessageChannel并没有本质上的不同,使用BasicMessageChannel传递一个数据,做一系列操作再返回数据,给我的感觉效果是一样的。为了验证自己的想法,大概翻了一下源码:
首先看两者的发送数据方法:

  ///BasicMessageChannel
  Future<T> send(T message) async {
    return codec.decodeMessage(await BinaryMessages.send(name, codec.encodeMessage(message)));
  }

  ///MethodChannel
  Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
    assert(method != null);
    final ByteData result = await BinaryMessages.send(
      name,
      codec.encodeMethodCall(MethodCall(method, arguments)),
    );
    if (result == null) {
      throw MissingPluginException('No implementation found for method $method on channel $name');
    }
    final T typedResult = codec.decodeEnvelope(result);
    return typedResult;
  }

两者内部的实现逻辑基本上是一样的,编码成二进制、BinaryMessages发送、解码。都是调用的BinaryMessages的send方法,唯一区别就是编解码器codec的不同。下面来看看codec的实现:

  ///BasicMessageChannel    StandardMessageCodec
  ByteData encodeMessage(dynamic message) {
    if (message == null)
      return null;
    final WriteBuffer buffer = WriteBuffer();
    writeValue(buffer, message);
    return buffer.done();
  }

  ///MethodChannel     StandardMethodCodec
  const StandardMethodCodec([this.messageCodec = const StandardMessageCodec()]);

  ByteData encodeMethodCall(MethodCall call) {
    final WriteBuffer buffer = WriteBuffer();
    messageCodec.writeValue(buffer, call.method);
    messageCodec.writeValue(buffer, call.arguments);
    return buffer.done();
  }

我们可以看到StandardMethodCodec中的messageCodec默认为StandardMessageCodec,而且编码方法和StandardMessageCodec的编码方法是一样的。默认实现就是相当于把方法名和参数封装成了一个MethodCall对象,再通过BasicMessageChannel传递。

Api

//发送
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {}
//接收
void setMethodCallHandler(Future<dynamic> handler(MethodCall call)) {}

Api不再赘述,设计和使用都类似于BasicMessageChannel,发送的时候传入方法名和参数,返回一个Future来监听响应。接收的时候传入一个高阶函数,参数为收到的信息,返回值为要响应的数据。

示例代码

依然是一个flutter和android相互调用的demo。
flutter:

PluginChannel.listenMethod();

onPressed: () {
   PluginChannel.sendBasicMessage();
},

class PluginChannel {
  static const _methodChannelName = "study_3/methodChannel";
  static const _methodChannel = MethodChannel(_methodChannelName);

  static void invokeMethod() {
    _methodChannel.invokeMethod("getAge", {"name": "lili"}).then((result) {
      print('flutter receive response:$result');
    });
  }

  static void listenMethod() {
    _methodChannel.setMethodCallHandler((methodCall) async {
      print('flutter listen:$methodCall');
      return "男";
    });
  }
}

android:

private val METHOD_CHANNEL = "study_3/methodChannel"
override fun onCreate(savedInstanceState: Bundle?) {
    methodChannelDemo()
}


    private fun methodChannelDemo(){
        MethodChannel(this.flutterView,METHOD_CHANNEL)
                .setMethodCallHandler { methodCall, result ->
                    println("android listen:${methodCall.method} \t ${methodCall.arguments}")
                    when(methodCall.method){
                        "getAge" -> {
                            result.success(getAge(methodCall.argument<String>("name")))
                        }
                    }


                    MethodChannel(this.flutterView,METHOD_CHANNEL)
                            .invokeMethod("getSex", mapOf(Pair("name","tom")), object : MethodChannel.Result {
                                override fun notImplemented() {
                                    println("android receive notImplemented")
                                }

                                override fun error(p0: String?, p1: String?, p2: Any?) {
                                    println("android receive error")
                                }

                                override fun success(p0: Any?) {
                                    println("android receive response:$p0")
                                }
                            })
                }
    }

    private fun getAge(name:String?): Int{
        return when(name){
            "lili" -> 18
            "tom" -> 19
            "allen" -> 20
            else -> 0
        }
    }

日志:

I/System.out( 9700): android listen:getAge {name=lili}
I/flutter ( 9700): flutter receive response:18
I/flutter ( 9700): flutter listen:MethodCall(getSex, {name: tom})
I/System.out( 9700): android receive response:男

EventChannel

Api

EventChannel并没有分别提供发送和收听消息的方法,它只提供了一个receiveBroadcastStream方法,用于发送消息,同时返回一个流(Stream),用于监听平台插件成功返回的所有事件信息,这个流可以被监听不止一次。因此我们可以用于native端需要持续传递数据到flutter的情况,比如监听电量,调用摄像头等等。

  Stream<dynamic> receiveBroadcastStream([dynamic arguments]) {
    final MethodChannel methodChannel = MethodChannel(name, codec);
    StreamController<dynamic> controller;
    controller = StreamController<dynamic>.broadcast(onListen, onCancel);
    return controller.stream;
  }

上面是receiveBroadcastStream的抽象代码,总共做了两件事:

  • 创建一个MethodChannel,传入自己的name和codec属性。
  • 创建一个StreamController,并返回流。
    StreamController.broadcast是一个命名构造函数,它返回一个广播流,可以不止一次被监听,它是惰性的,在首次被订阅时调用onListen,不再订阅时调用onCancel。如果之后继续订阅,则再次调用onListen。
    那么我们来继续看下receiveBroadcastStream中的onListen做了什么:
      BinaryMessages.setMessageHandler(name, (ByteData reply) async {
        if (reply == null) {
          controller.close();
        } else {
          try {
            controller.add(codec.decodeEnvelope(reply));
          } on PlatformException catch (e) {
            controller.addError(e);
          }
        }
        return null;
      });
      try {
        await methodChannel.invokeMethod<void>('listen', arguments);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
          context: 'while activating platform stream on channel $name',
        ));
      }

可以看到onListen中依然是做了两件事情:

  • 设置回调来接收平台插件返回的消息。
  • 通过之前创建的MethodChannel发送一条消息,方法名定死为listen,参数为receiveBroadcastStream传入的可选参数。
    最后再来看下receiveBroadcastStream中的onCancel:
      BinaryMessages.setMessageHandler(name, null);
      try {
        await methodChannel.invokeMethod<void>('cancel', arguments);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
          context: 'while de-activating platform stream on channel $name',
        ));
      }

依然两件事:

  • 移除这个通道。
  • 用之前创建的MethodChannel发送一条消息,方法名定死为cancel,参数为receiveBroadcastStream传入的可选参数。

可以想象,native平台肯定也定义了一个MethodChannel,用来接收listen和cancel方法,我们来验证一下,以Android端为例,EventChannel部分源码:

       public void onMessage(ByteBuffer message, BinaryReply reply) {
            MethodCall call = EventChannel.this.codec.decodeMethodCall(message);
            if (call.method.equals("listen")) {
                this.onListen(call.arguments, reply);
            } else if (call.method.equals("cancel")) {
                this.onCancel(call.arguments, reply);
            } else {
                reply.reply((ByteBuffer)null);
            }
        }

我们可以看到,当收到消息的时候,有三种情况:

  • 收到listen方法,则调用onListen。
  • 收到cancel方法,则调用onCancel。
  • 其他,则返回null,此时flutter端收到null则会关闭这个流。

示例代码

flutter:

class PluginChannel {

  static const _eventChannelName = "study_3/eventChannel";
  static const _eventChannel = EventChannel(_eventChannelName);

  static void event() {
    _eventChannel.receiveBroadcastStream("event arg")
        .listen((result) {
      print('flutter listen:$result');
    });
  }
}

android:

    private fun eventChannelDemo(){
        EventChannel(this.flutterView,EVENT_CHANNEL)
                .setStreamHandler(object : EventChannel.StreamHandler {
                    override fun onListen(p0: Any?, events: EventChannel.EventSink?) {
                        println("android onListen:$p0")

                        events?.success(1)
                        events?.success(2)
                        events?.success(3)
                        events?.success(4)
                        events?.endOfStream()
                        events?.success(5)
                    }

                    override fun onCancel(p0: Any?) {
                        println("android onCancel:$p0")
                    }
                })
    }

日志:

I/System.out( 9271): android onListen:event arg
I/flutter ( 9271): flutter listen:1
I/flutter ( 9271): flutter listen:2
I/flutter ( 9271): flutter listen:3
I/flutter ( 9271): flutter listen:4
I/System.out( 9271): android onCancel:event arg

完整代码地址

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

推荐阅读更多精彩内容