首页
/ 从 PlatformMessages 到 Channels:Flutter 平台通道迁移指南与架构演进解读

从 PlatformMessages 到 Channels:Flutter 平台通道迁移指南与架构演进解读

2026-09-07 13:34:05作者:牧宁李

本文是围绕 Flutter 官方 Wiki 归档文档 Upgrading-Flutter-projects-from-using-PlatformMessages-to-using-channels.md 展开的技术解读。该文档记录了 Flutter 历史上一次影响深远的基础设施改造:2017 年 3 月之前,Dart 代码与 Android/iOS 宿主平台之间只能通过字符串/JSON 报文手工收发,之后则被统一的「命名通道(Channel)+ 编解码器(Codec)」模型取代。读者将借此理解通道、方法调用、事件流与四种 Codec 的设计动机,掌握把旧式消息收发代码迁移到通道 API 的具体改写方法,并结合当前仓库源码看清 BasicMessageChannelMethodChannelEventChannel 在现代 Flutter 框架中的真实落点。

一、背景:为什么会有这次迁移

在 2017 年 3 月 17 日合并的 [flutter/flutter#8837] 与 [flutter/engine#3482] 两次 Pull Request 之前,Flutter 应用(Dart 侧)与宿主平台应用(Android / iOS 原生侧)之间的通信,依赖的是「发送和接收字符串/JSON 消息」的平铺方法,分别定义在三类类型上:

  • Dart 侧:PlatformMessages
  • Android 侧:FlutterView
  • iOS 侧:FlutterViewController

这套旧 API 存在明显的工程痛点:

  1. 层级过低:一切通信都退化为裸字符串或 JSON 文本,业务语义(这是一个方法调用还是一条事件流?)需要在消息体里手工约定;
  2. 样板代码多:方法名、参数包、结果包都要手工拼装 Map,错误处理无处安放;
  3. 三端不对称:Dart、Android、iOS 三端各自暴露一套形态不一致的收发入口,维护成本高。

新方案:通道(Channel)概念

迁移的核心是废除旧的字符串/JSON 收发方法(只保留发送未编码二进制消息的低层能力),转而引入通道这一更高层的抽象:

  • Dart 侧:PlatformMessageChannelPlatformMethodChannel
  • Android / iOS 侧:FlutterMessageChannelFlutterMethodChannel

设计目标非常明确(这也是理解后续所有 API 的关键):

  • 更高层的通信语义:支持异步方法调用(请求-响应)与事件流(推送);
  • 更少的样板与冗余:通道名 + Codec 只定义一次,处处复用;
  • 跨端 API 更对称:Dart、Android、iOS 三端面向同一套概念建模。

历史注记:该文档标题中的 PlatformMessageChannel 等命名属于迁移期的过渡命名。今日 Flutter 框架中,这些类的直接继承者是 BasicMessageChannelMethodChannelEventChannel,其实现位于 platform_channel.dart,具体演进详见本文第七节。

二、四种 Codec:消息的“编码协议”

新 API 中,通道通过显式指定的编解码器(Codec) 来决定消息在线缆上的表示方式,这也是「对称性」的根基:只要三端选用同一 Codec,二进制字节就能被正确互解。文档给出四种选择:

Codec 编码形态 典型场景
BinaryCodec 未编码的原始二进制 直接传 ByteData 等裸字节
StringCodec UTF-8 编码字符串 纯文本消息
JSONCodec(消息)/ JSONMethodCodec(方法) UTF-8 编码的 JSON 可空性简单的结构化数据
StandardCodec(消息)/ StandardMethodCodec(方法) JSON 风格值的高效二进制序列化 通用默认选择

其中 Standard 系列尤其值得注意:它采用对 JSON 风格值的二进制序列化,效率更高,并且支持将缓冲区(buffer)作为叶子值内嵌传递,例如 Dart 的 TypedData、Java 的原始类型数组(primitive arrays)、Cocoa 的 NSData。这使它能表达比纯 JSON 更丰富的类型。

在当前仓库源码中,这四类 Codec 均真实存在并有完整文档注释:

所有 Codec 都实现自抽象基类 MessageCodec,其契约只有一对方法 encodeMessage/decodeMessage(编码失败应视为编程错误而抛异常)。

三、Flutter 侧迁移:四类场景逐一改写

3.1 通道定义:一次声明,处处使用

新 API 中,你在一个位置集中定义所需通道的名字与类型:

var fooChannel = new PlatformMessageChannel<String>('foo', const StringCodec());
var barChannel = new PlatformMethodChannel('bar', const JSONMethodCodec());

一旦建好,通道对象可以在多处使用,而无需重复携带创建信息——这正是「减少样板代码」目标的直接体现。现代 Dart 写法中,通道通常以 static const 声明并复用(见 main.dart):

static const MethodChannel methodChannel = MethodChannel('samples.flutter.io/battery');
static const EventChannel eventChannel = EventChannel('samples.flutter.io/charging');

命名约定:通道名使用反向域名风格(如 samples.flutter.io/battery)以避免冲突。通道的逻辑身份就是它的名字,同名通道会互相干扰通信;框架内建的通道保证 FIFO 顺序投递。

3.2 向平台发送消息

旧代码:

String reply = await PlatformMessages.sendString('foo', myString);

新代码:

String reply = await fooChannel.send(myString);

差异一目了然:通道对象本身携带了 'foo' 名字与 StringCodec,send 不再需要传通道名与编码类型。

3.3 接收来自平台的消息

旧代码:

PlatformMessages.setStringMessageHandler('foo', (String message) async {
  // do something, then
  return reply;
});

新代码:

fooChannel.setMessageHandler((String message) async {
  // do something, then
  return reply;
});

处理器(Handler)注册从「全局函数 + 名字参数」收敛为「通道对象方法」,Dart 侧处理器的返回值会被自动编码回传平台。

3.4 调用平台方法(Dart → 平台)

旧代码需要手工组装 {method, args} 信封消息,甚至有两种写法:

var arguments = { 'argA': 'hello', 'argB': 42 };
var message = {
  'method': 'someMethod',
  'args': <Map<String, dynamic>>[arguments],
};
dynamic reply = await PlatformMessages.sendJSON('bar', message);
// what about errors?
var arguments = { 'argA': 'hello', 'argB': 42 };
dynamic reply = await PlatformMessages.invokeMethod(
  'bar',
  'someMethod',
  <Map<String, dynamic>>[arguments],
);
// what about errors?

注意旧 API 末尾的 // what about errors?——错误处理在旧模型里根本没有着落

新代码把方法名与参数直接交给通道,并且第一次把错误纳入一等公民:

try {
  dynamic result = await barChannel.invokeMethod(
    'someMethod',
    { 'argA': 'hello', 'argB': 42 },
  );
  // use result
} on PlatformException catch(e) {
  // handle error
}

PlatformException 是现代框架中方法调用失败的标准异常类型(定义于 message_codec.dart)。从当前源码看,MethodChannel.invokeMethod 的完整语义是:先用 codec 把 MethodCall 编码为二进制经 BinaryMessenger.send 发出;若返回 null(平台侧没有实现)则抛 MissingPluginException,否则用 decodeEnvelope 解码,失败时抛 PlatformException(见 platform_channel.dart)。

3.5 接收来自平台的方法调用(平台 → Dart)

旧代码同样手工解析信封:

PlatformMessages.setJSONMessageHandler('bar', (dynamic methodCall) async {
  String method = methodCall['method'];
  List arguments = methodCall['args'];
  // handle call then
  return result;
  // but what about errors?
});

新代码引入了类型化的 MethodCall 对象,错误用异常表达:

barChannel.setMethodCallHandler((MethodCall call) async {
  String method = call.method;
  dynamic arguments = call.arguments;
  // handle call then
  return result;
  // or
  throw new PlatformException(errorCode, anErrorMessage, someDetails);
});

MethodCall 正是「方法名 + 参数」这一对概念的类型化载体,构造与读写都在 message_codec.dart 中定义。

迁移实例可参考 examples/platform_channel/lib/main.dart:页面通过 MethodChannel 主动 invokeMethod('getBatteryLevel') 查询电量并以 on PlatformException 处理 NO_BATTERY 分支,同时用 EventChannelreceiveBroadcastStream().listen(...) 订阅充电状态推送。

四、Android 侧迁移:io.flutter.plugin.common

Android 侧的迁移与 Flutter 侧对称,使用 io.flutter.plugin.common 下的 FlutterMessageChannelFlutterMethodChannel。发送消息并可选处理回执:

FlutterView view = ...
FlutterMessageChannel<String> fooChannel =
  new FlutterMessageChannel<>(view, "foo", StringCodec.INSTANCE);

fooChannel.send(myString);

// or if you need to handle a reply:

fooChannel.send(myString, new ReplyHandler<String>() {
  public void onReply(String reply) {
    // do something with reply
  }
});

在今天的 Android embedding 中,对应概念已演化为基于 FlutterEngine.getDartExecutor()MethodChannelEventChannel。仓库内完整的现代示例见 MainActivity.java,它采用 FlutterActivity + configureFlutterEngine 的现代嵌入方式,注册同名通道并通过 MethodCallHandler.onMethodCall 分发 "getBatteryLevel" 方法,最终以 result.success(...) / result.error("UNAVAILABLE", ...) / result.notImplemented() 三态回应 Dart:

new MethodChannel(flutterEngine.getDartExecutor(), BATTERY_CHANNEL).setMethodCallHandler(
  new MethodCallHandler() {
    @Override
    public void onMethodCall(MethodCall call, Result result) {
      if (call.method.equals("getBatteryLevel")) {
        int batteryLevel = getBatteryLevel();
        if (batteryLevel != -1) {
          result.success(batteryLevel);
        } else {
          result.error("UNAVAILABLE", "Battery level not available.", null);
        }
      } else {
        result.notImplemented();
      }
    }
  }
);

这一侧同样有可运行的自动化验证:仓库提供了针对示例 App 的冒烟测试 platform_channel_test.dart,它 pump 一帧并断言 'Battery level: ' 文本存在,证明通道 UI 能正常构建与渲染。

五、iOS 侧迁移:FlutterChannels.h

iOS 侧使用 FlutterChannels.h 中的 FlutterMessageChannelFlutterMethodChannel。创建通道时需要显式传入 binaryMessenger(宿主视图控制器)与 codec 单例:

FlutterViewController controller = ...
FlutterMessageChannel* fooChannel =
  [FlutterMessageChannel messageChannelWithName:@"foo"
                                binaryMessenger:controller
                                          codec:[FlutterStringCodec sharedInstance]];

[fooChannel sendMessage:myString];

// or if you need to handle a reply:

[fooChannel sendMessage:myString replyHandler:^(id reply) {
  // do something with (NSString*)reply
}];

与 Android 侧「构造函数 + 单例 Codec」的 Java 风格不同,Objective-C 侧采用类方法 +messageChannelWithName:binaryMessenger:codec: 与共享 codec 实例([FlutterStringCodec sharedInstance]),这是当时 API 对称性设计在语言惯例层面的体现。仓库中的迁移对照示例见 AppDelegate.m

从源码结构看,iOS 侧的对应头文件仍位于 engine 的 Darwin 平台层(engine/src/flutter/shell/platform/darwin/ios/framework/Headers),说明这套通道体系从 2017 年引入后一直是 Flutter 与宿主平台通信的骨干设施。

六、一次典型迁移的检查清单

把上述四节串起来,一个完整的「旧式消息收发 → 通道化」迁移可以归纳为五步:

  1. 盘点通信点:找出 Dart 代码中所有 PlatformMessages.sendString / sendJSON / invokeMethod / set*MessageHandler 调用,以及 Android FlutterView、iOS FlutterViewController 上对应的消息收发方法;
  2. 划定通道边界:按「消息通道(Message)」「方法通道(Method)」两种职责分类;每一条业务链路选择一个唯一的通道名;
  3. 确定 Codec:纯文本用 StringCodec,简单 JSON 结构用 JSONMethodCodec,需要传递 TypedData/字节数组/NSData 等叶子缓冲区时用 StandardMethodCodec
  4. 三端同步改造:Dart / Android / iOS 使用完全相同的通道名与 Codec 成对创建通道,先改 Dart 侧收发点,再改原生侧对应实现;
  5. 补上错误路径:方法调用一律包进 try / on PlatformException(Dart),原生侧用 Result.error / PlatformException 返回错误语义,消灭旧模型「errors 无处安放」的空白。

七、现代框架中的延续:从迁移命名到今天的三类通道

原 Wiki 文档是 2017 年的「当时态」迁移指南,其中 PlatformMessageChannelPlatformMethodChannel 等命名在后续演进中被重构为如今开发者熟悉的形态。若对照当前框架源码 platform_channel.dart,可以清晰看到这套设计思想的最终稳定形态:

  • BasicMessageChannel:通用异步消息通道,对应文档中的「消息收发」能力,任意方向均可达;
  • MethodChannel:方法调用通道,默认使用 StandardMethodCodecinvokeMethod 抛出 MissingPluginException(无实现)或解码失败的 PlatformException;其文档注释明确「同名通道会相互干扰通信」,呼应了通道命名需全局唯一的原则;
  • EventChannel:事件流通道,用于平台 → Dart 的持续推送,是原文档「事件流(event streams)」目标的最终落地;
  • 以及 OptionalMethodChannelplatform_channel.dart)——在插件可选场景下 invokeMethod 未命中时返回 null 而非抛异常。

换句话说,迁移文档所确立的三条原则——高层通信语义、少样板、三端对称——至今仍是 Flutter 平台通道架构的基石。现代开发者几乎不会遇到需要迁移 PlatformMessages 的存量代码,但理解这段历史,恰恰能帮你搞明白为什么要写 MethodChannel('samples.flutter.io/battery'),为什么调用要用 try/on PlatformException,以及为什么「通道名 + Codec」必须与原生侧严格一致。

参考资料(仓库内,可继续深入)

登录后查看全文
热门项目推荐
相关项目推荐