Flutter 与 Native 通信详解(上):原理探究

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和平台的耦合度降到最低。

image

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端是一个协议 FlutterBinaryMessengerFlutterViewController遵守并实现了这个协议。

image

其主要实现了发送二进制消息和设置消息处理回调的方法,如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,反之亦然。


image

对于字符串、列表、字典的编码会稍微复杂一些。字符串使用UTF-8编码得到的二进制数据是长度不定的,因此会在写入type后,先写入一个代表二进制数据长度的size,再写入数据。列表和字典则是写入type后,先写入一个代表列表或字典中元素个数的size,再递归调用writeValue方法将其元素依次写入。

至于消息编解码器的具体用法,就要说到 Platform channel 了,其实就是对于以上介绍的几个通信基础要素的组合封装,这里由于篇幅问题,下篇文章再说。

总结

到这里,想必你已经理解了flutter和平台端通信的原理:通过消息信使(BinaryMessenger)来异步的收发二进制消息,每个消息都有对应的消息渠道(channel)来区分不同的消息用途,然后使用不同的消息编解码器(Codec)对二进制数据进行序列化与反序列化,最后通过注册的消息处理器(MessageHandler)来处理并回复对应的消息。


image

参考

Flutter Platform Channels
深入理解Flutter Platform Channel
flutter engine 源码
Writing custom platform-specific code

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

推荐阅读更多精彩内容