Flutter 平台通道(Platform Channels)简明总结
Don't communicate by sharing memory; share memory by communicating. -- R. Pike
Flutter APP 开发时, 为满足核心业务功能(比如全盘文件扫描, 视频转码, 图片处理等)的可重用, 高性能, 可维护等需求, 就必然要求在平台侧实现这些核心功能.
此外, 由于 Flutter 仅在引擎中向上暴露了很 少一部分平台 API 的封装, 如果想要访问硬件, 或直接使用系统调用或进行库函数调用等, 在 Flutter 侧也是不能完成的.
注: 使用 Dart FFI (外部函数接口) 可直接同 C 库进行互操, 本文暂不进行 FFI 相关讨论.
这也引出了一些问题:
- 如何在平台侧和 Flutter 侧进行数据通信?
- 平台侧和 Flutter 侧的关系到底是什么样的?
要简明地回答第一个问题, 下面先尽量简明地来回答第二个问题 :)
平台部分和 Flutter 部分的关系
在 macOS 或 iOS 上, 我们通常说的 "Flutter App" 中的 "Flutter 部分" 最终打包为两个动态库(Flutter.framework 和 App.framework)并被链接到 APP 二进制文件.
三者依赖关系是: "平台 APP 二进制文件" ---> "Flutter.Framework"(包含 Flutter 引擎 和 Flutter framework) ---> "App.frameowrk"(自己写的 Dart 代码).
通过
file
和otool -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');
});
对比可以发现消息通道要比二进制消息通道简单. 从原理上看, 消息通道底层仍然依赖二进制消息通道发送和接收消息, 只是会自动对消息进行编解码.
如下是消息通道的特点:
- 所有通信还是底层依赖二进制消息通道.
- 当释放后, message handler 也随之释放.
- 它是轻量级且无状态的.
- 在不同位置创建的两个消息通道, 只要通道名字相同, 实际它们就是同一个通道.
另外, 在 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 和平台代码间进行方法互调. 它使用标准化的消息封装手段来将诸如方法名, 参数, 调用成功与否的响应等信息进行封装. 针对这些信息的编解码有对应的编解码器实现(主要是 JSONMethodCodec
和 StandardMethodCodec
).
如下是方法通道的例子:
// 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 有两个生命期事件方法, 分别是 onListen
和 onCancel
, 对应的就是流的开始和结束. 只有在流开始后才可以向其中放入事件, 流终止后就不能放入事件了.
事件通道使用的编解码器是一个对应的方法编解码器, 这样才可以识别成功或错误事件. 它的两个生命期事件方法通过内部方法通道被调用, 而这一切都在同一个逻辑上的事件通道上完 成.
如果不在事件通道的 Dart 侧调用 cancel, 还可以在平台侧的 sink 上调用 endOfStream
方法, 它实际发送的是一个 null 消息到二进制消息通道上, 这样 Dart 侧会自动关闭事件通道.
总结
平台通道是一种线程间通信手段, 在使用时还需要注意如下:
- 线程限制: 在平台侧发送或接收消息时, 必须是在平台的主线程进行. 而在 Dart 侧发送或接收消息时, 需要在 Main Isolate.
- 异常: 在 handler 抛出的异常, 会被 Framework 捕获并打印出来, 然后一个 null reply 会被发回到对方.
- handler 的生命期: 和对应的 Flutter View 生命期一致, 当然也可以对同名通道设置一个 null handler 提前将 handler 释放.
- 当没有对应 handler 注册时, 框架会默认给消息发送方回复 null reply.
- 消息通信是异步的.
- 为了保持平台通道名字的唯一性, 最好是将名字前缀 domain, 因为无法避免 APP 使用的插件有同名通道.
完结撒花.