Skip to main content

Flutter 平台通道(Platform Channels)简明总结

Don't communicate by sharing memory; share memory by communicating. -- R. Pike

Flutter APP 开发时, 为满足核心业务功能(比如全盘文件扫描, 视频转码, 图片处理等)的可重用, 高性能, 可维护等需求, 就必然要求在平台侧实现这些核心功能.

此外, 由于 Flutter 仅在引擎中向上暴露了很少一部分平台 API 的封装, 如果想要访问硬件, 或直接使用系统调用或进行库函数调用等, 在 Flutter 侧也是不能完成的.

注: 使用 Dart FFI (外部函数接口) 可直接同 C 库进行互操, 本文暂不进行 FFI 相关讨论.

这也引出了一些问题:

  1. 如何在平台侧和 Flutter 侧进行数据通信?
  2. 平台侧和 Flutter 侧的关系到底是什么样的?

要简明地回答第一个问题, 下面先尽量简明地来回答第二个问题 :)

平台部分和 Flutter 部分的关系

在 macOS 或 iOS 上, 我们通常说的 "Flutter App" 中的 "Flutter 部分" 最终打包为两个动态库(Flutter.framework 和 App.framework)并被链接到 APP 二进制文件.

三者依赖关系是: "平台 APP 二进制文件" ---> "Flutter.Framework"(包含 Flutter 引擎 和 Flutter framework) ---> "App.frameowrk"(自己写的 Dart 代码).

通过 fileotool -L 可以清晰地查看这三者的关系.(可以发现 App.framework 是运行时动态链接的)

Flutter 侧入口可以被包含在平台提供的 View/Window/Controller 对象中, 在平台对象的初始化过程中, 去做诸如启动引擎, 启动 Dart 虚拟机, 执行 Dart main 函数, 初始化框架, 开始第一帧渲染等操作, 接着便是 vSync 信号驱动引擎再由引擎驱动 Framework 持续运行 render pipeline.

相信所有 Flutter 开发者都知道: Flutter 本质上是宿主 APP 的一个组成部分.

Flutter APP = 平台部分 + Flutter 部分

其中:

  • 平台部分: 平台代码 + Flutter 引擎
  • Flutter 部分: Flutter framework + 我们的 Dart 代码

由于 Dart 的"单线程体质", Flutter 开发过程中通常没有这样的需求去促使我们创建新的 Isolate. 单 Isolate 环境下, 所有 Dart 代码在 Main Isolate 对应的线程中运行.

相对平台侧主线程(通常作为平台侧 UI 线程)而言, Main Isolate 对应的线程就是一个普通的后台线程. 因此平台侧和 Flutter 侧的通信可以简化为同一进程内两个线程间通信.

回到顶上那句话: Don't communicate by sharing memory; share memory by communicating. 这句话放到进程/线程/协程间数据传递都适用, 大部分成熟的语言或框架都有提供自己的跨线程/协程消息通信手段, chan(Go), channel(Rust), 还有我们的 Flutter Platform Channels.

而 Dart Main Isolate 线程和平台 Main 线程间通信(为什么必须是这两个 Main? 详见官方文档解释 Task Queue API), 可以交给 Flutter 为我们提供的成熟的 平台通道 们去完成.

平台通道本质上是一个对象, 它拥有一个通道名称, 和一个编解码器, 可以在发送或接收消息时将底层的二进制消息进行s编解码.

目前, Flutter 提供如下平台通道(Platform Channels):

  • 二进制消息通道: 是其他平台通道的基础, 传递被编码的二进制数据.
  • 基本消息通道: 传递简单消息, 有对应的各种消息编解码器.
  • 方法调用通道: 传递自定义格式的方法调用消息, 并提供返回值及调用成功与否的回调, 有对应的各种消息编解码器.
  • 事件流通道: 在消息通道上发展出来的单向(目前仅支持从平台侧流到 Flutter 侧)的流式消息通道.

我们再回到第一个问题: 如何在平台侧和 Flutter 侧进行双向通信? 现在就一目了然了.

余下内容介绍这些通道的特性和 API 用法. 下方图片以及演示代码均源自文末的参考文档, 仅改动部分代码以便简化描述.

平台通道 API 用法

二进制消息通道

可以在下图中看到消息传递路径为:

  • 平台到 Flutter 时: 平台代码(平台侧) -> Flutter 引擎(平台侧) -> Flutter framework(Flutter 侧) -> Flutter 代码(Flutter 侧)
  • Flutter 到平台时: 和上面流程相反.

各个通道的图都是类似, 因此后续不再重复描述.

Flutter 侧向平台侧发送二进制消息:

final WriteBuffer buffer = WriteBuffer()
..putFloat64(3.1415)
..putInt32(12345678);
final ByteData message = buffer.done();
await BinaryMessages.send('foo', message);
print('Message sent, reply ignored');

平台侧接收二进制消息:

let flutterVC = window?.rootViewController as! FlutterViewController

flutterVC.setMessageHandlerOnChannel("foo") { (message, reply) in
let x : Float64 = message.subdata(in: 0..<8).withUnsafeBytes { $0.pointee }
let n : Int32 = message.subdata(in: 8..<12).withUnsafeBytes { $0.pointee }
reply(nil)
}

平台侧向 Flutter 侧发送二进制消息:

let message = Data(capacity: 12)
let x : Float64 = 3.1415
let n : Int32 = 12345678
message.append(UnsafeBufferPointer(start: &x, count: 1))
message.append(UnsafeBufferPointer(start: &n, count: 1))

flutterView.send(onChannel: "foo", message: message) {(_) -> Void in
os_log("Message sent, reply ignored")
}

Flutter 侧接收二进制消息:

BinaryMessages.setMessageHandler('foo', (ByteData message) async {
final readBuffer = ReadBuffer(message);
final x = readBuffer.getFloat64();
final n = readBuffer.getInt32();
print('Received $x and $n');
return null;
});

消息通道(BasicMessageChannel 最简单形式平台通道): 名字 + 编解码器

在使用二进制消息通道时有非常多需要考虑的地方, 特别是消息序列化的问题. Flutter 因此在其上提供了几种类型的平台通道, 极大方便了使用.

下图可以和二进制通道对比, 在其中消息通道扩展了二进制消息通道的功能:

通过消息通道将消息编解码的例子如下所示:

// 创建消息通道
const channel = BasicMessageChannel<String>('foo', StringCodec());

// 发送消息
final String reply = await channel.send('Hello, world');

print(reply);

// 设置消息接收 handler, 收到后需要发送响应
channel.setMessageHandler((String message) async {
print('Received: $message');
return 'Hi from Dart';
});

在平台侧:

// 创建对应名字的消息通道
let channel = FlutterBasicMessageChannel(
name: "foo",
binaryMessenger: controller,
codec: FlutterStringCodec.sharedInstance()
)

// 向 Dart 侧发送消息
channel.sendMessage("Hello, world") {(reply: Any?) -> Void in
os_log("%@", type: .info, reply as! String)
}

// 设置消息接收 handler, 接收后需要发送响应
channel.setMessageHandler { (message, reply) in
os_log("Received: %@", type: .info, message as! String)
reply("Hi from iOS")
}

如果相同的消息要通过二进制消息通道发送, 则需要像下面这样:

// 创建消息编解码器
const codec = StringCodec();

// 发送消息并对响应解码
final String reply = codec.decodeMessage(
await BinaryMessages.send(
'foo',
codec.encodeMessage('Hello, world'),
),
);
print(reply);

// 设置消息接收 Handler, 同样通过编解码器解码或编码消息
BinaryMessages.setMessageHandler('foo', (ByteData message) async {
print('Received: ${codec.decodeMessage(message)}');
return codec.encodeMessage('Hi from Dart');
});

对比可以发现消息通道要比二进制消息通道简单. 从原理上看, 消息通道底层仍然依赖二进制消息通道发送和接收消息, 只是会自动对消息进行编解码.

如下是消息通道的特点:

  1. 所有通信还是底层依赖二进制消息通道.
  2. 当释放后, message handler 也随之释放.
  3. 它是轻量级且无状态的.
  4. 在不同位置创建的两个消息通道, 只要通道名字相同, 实际它们就是同一个通道.

另外, 在 Flutter 框架中定义有如下的编解码器可用:

  • StringCodec: 使用 UTF-8 格式编解码字符串, 对应消息通道类型是 BasicMessageChannel<String>
  • BinaryCodec: 实现消息的二进制序列化以供特殊用途, 对应消息通道类型是 BasicMessageChannel<ByteData>
  • JSONMessageCodec: 处理 JSON 的编解码, 同样使用 UTF-8 编码到二进制消息, 对应消息通道类型是 BasicMessageChannel<dynamic>
  • StandardMessageCodec: 它支持更广泛的数据类型, 比如 list, map(以及非字符串作为 key 的 map), 它将消息编码为一个便于编解码的自定义二进制形式. 在传递复杂数据时, 它也是推荐的编解码方式.

上述编解码器在 Dart 侧和平台侧各有一个对应实现, 都通过 Flutter 框架提供.

由于消息通道本质上可以同任意编解码器协同工作, 因此还可以实现自己的编解码器, 在 Dart 侧和平台侧实现对应接口的编解码器即可. 任何编解码器都需要支持 null 消息(因为消息通道默认的响应就是 null).

方法通道(MethodChannel)

消息通道是消息通信的基础, 使用场景也比较广泛, 但如果要调用平台方法并传递参数, 一个办法是通过消息通道将这些信息都传过去, 但要处理的东西就更多了, 方法名称, 参数类型, 响应接收等...

正因为如此, Flutter 还提供了方法通道来方便平台和 Dart 间的方法互调.

方法通道也是一种平台通道, 用于在 Dart 和平台代码间进行方法互调. 它使用标准化的消息封装手段来将诸如方法名, 参数, 调用成功与否的响应等信息进行封装. 针对这些信息的编解码有对应的编解码器实现(主要是 JSONMethodCodecStandardMethodCodec).

如下是方法通道的例子:

// dart 侧

// 创建名为 foo 的方法通道
const channel = MethodChannel('foo');

// 调用方法 bar, 并传入 world 参数
final String greeting = await channel.invokeMethod('bar', 'world');

// 获取结果
print(greeting);
// 平台侧

// 创建名为 foo 的方法通道, 使用的 messageer 是对应的 Flutter 视图控制器
let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterVC)

// 设置方法调用消息接收 Handler, 针对不同方法名称, 解析参数并进行调研, 然后通过 result 返回结果
channel.setMethodCallHandler { (call: FlutterMethodCall, result: FlutterResult) in
switch (call.method) {
case "bar":
result("Hello, \(call.arguments as! String)")
default:
result(FlutterMethodNotImplemented) // 未实现的方法, 实际就是 null 响应
}
}

上面的 Dart 侧方法通道在内部实际也是通过二进制消息通道来传递数据封装的, 如下所示:

// 创建标准方法编解码器(注意不是之前的标准消息编解码器)
const codec = StandardMethodCodec();

// 通过二进制消息通道发送 "方法调用" 消息
final ByteData reply = await BinaryMessages.send('foo', codec.encodeMethodCall(MethodCall('bar', 'world')));

if (reply == null)
throw MissingPluginException();
else
print(codec.decodeEnvelope(reply));

而在平台侧也是和上述类似的, 就是把平台侧是二进制消息通道上封装了一层方法消息编解码过程.

在参数传递上, 由于使用的是标准编解码器, 因此可以传递同质/异质的列表或字典:

// Dart 侧传递列表或字典参数(平台侧同理)
await channel.invokeMethod('bar'); // 如果没有参数, 表示参数是 null 的
await channel.invokeMethod('bar', <dynamic>['world', 42, pi]); // 异质列表
await channel.invokeMethod('bar', <String, dynamic>{ // 异质字典
name: 'world',
answer: 42,
math: pi,
}));

关于方法调用的错误处理:

const channel = MethodChannel('foo');

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();
}
});
let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterVC)

let name = "bar" // or "baz", or "unknown"
let value = "world"
// 平台侧处理调用 Dart 侧方法时候的错误
channel.invokeMethod(name, arguments: value) { (result: Any?) -> Void in
if let error = result as? FlutterError {
os_log("%@ failed: %@", type: .error, name, error.message!)
} else if FlutterMethodNotImplemented.isEqual(result) {
os_log("%@ not implemented", type: .error, name)
} else {
os_log("%@", type: .info, result as! NSObject)
}
}

// 被 Dart 侧调用时候的处理
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar":
result("Hello, \(call.arguments as! String)")
case "baz":
result(FlutterError(code: "400", message: "This is bad", details: nil))
default:
result(FlutterMethodNotImplemented)
}

事件通道

事件通道是一种特殊的平台通道, 用于通过流式风格来传递消息(即消息流). 它底层仍然依赖二进制消息通道, 将平台侧的事件通过流的形式暴露给 Flutter 侧. 目前 Flutter 还没有实现将 Dart 侧的事件流暴露给平台侧.

如下是 Dart 侧接收事件流的代码:

// 创建事件通道
const channel = EventChannel('foo');

// 从通道上获取流, 并进行观察
channel.receiveBroadcastStream().listen((dynamic event) {
print('Received event: $event');
}, onError: (dynamic error) {
print('Received error: ${error.message}');
});

平台侧生成事件流(仅示例安卓):

class SensorListener(private val sensorManager: SensorManager) :
EventChannel.StreamHandler, SensorEventListener {
private var eventSink: EventChannel.EventSink? = null

// EventChannel.StreamHandler methods
override fun onListen(
arguments: Any?, eventSink: EventChannel.EventSink?) {
this.eventSink = eventSink
registerIfActive()
}
override fun onCancel(arguments: Any?) {
unregisterIfActive()
eventSink = null
}

// SensorEventListener methods.
override fun onSensorChanged(event: SensorEvent) {
eventSink?.success(event.values)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
if (accuracy == SensorManager.SENSOR_STATUS_ACCURACY_LOW)
eventSink?.error("SENSOR", "Low accuracy detected", null)
}
// Lifecycle methods.
fun registerIfActive() {
if (eventSink == null) return
sensorManager.registerListener(
this,
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
SensorManager.SENSOR_DELAY_NORMAL)
}
fun unregisterIfActive() {
if (eventSink == null) return
sensorManager.unregisterListener(this)
}
}
// Use of the above class in an Activity.
class MainActivity: FlutterActivity() {
var sensorListener: SensorListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
sensorListener = SensorListener(getSystemService(Context.SENSOR_SERVICE) as SensorManager)
val channel = EventChannel(flutterView, "foo")
channel.setStreamHandler(sensorListener)
}

override fun onPause() {
sensorListener?.unregisterIfActive()
super.onPause()
}

override fun onResume() {
sensorListener?.registerIfActive()
super.onResume()
}
}

可以看到在平台侧如果要生成流, 需要实现对应协议(StreamHandler), 通过 eventSink 发送事件. 在平台侧的 Stream Handler 有两个生命期事件方法, 分别是 onListenonCancel, 对应的就是流的开始和结束. 只有在流开始后才可以向其中放入事件, 流终止后就不能放入事件了.

事件通道使用的编解码器是一个对应的方法编解码器, 这样才可以识别成功或错误事件. 它的两个生命期事件方法通过内部方法通道被调用, 而这一切都在同一个逻辑上的事件通道上完成.

如果不在事件通道的 Dart 侧调用 cancel, 还可以在平台侧的 sink 上调用 endOfStream 方法, 它实际发送的是一个 null 消息到二进制消息通道上, 这样 Dart 侧会自动关闭事件通道.

总结

平台通道是一种线程间通信手段, 在使用时还需要注意如下:

  1. 线程限制: 在平台侧发送或接收消息时, 必须是在平台的主线程进行. 而在 Dart 侧发送或接收消息时, 需要在 Main Isolate.
  2. 异常: 在 handler 抛出的异常, 会被 Framework 捕获并打印出来, 然后一个 null reply 会被发回到对方.
  3. handler 的生命期: 和对应的 Flutter View 生命期一致, 当然也可以对同名通道设置一个 null handler 提前将 handler 释放.
  4. 当没有对应 handler 注册时, 框架会默认给消息发送方回复 null reply.
  5. 消息通信是异步的.
  6. 为了保持平台通道名字的唯一性, 最好是将名字前缀 domain, 因为无法避免 APP 使用的插件有同名通道.

完结撒花.

参考

  1. Flutter Platform Channels
  2. 官方文档: 平台通道
  3. 官方文档: 包和插件
  4. Dart Concurrency