面试备战 Flutter 25:Native 通信,Channel、PlatformView、网络与线程

面试备战 Flutter 25:Native 通信,Channel、PlatformView、Codec 与线程

Flutter Native 通信面试通常会连环追问:

  • MethodChannel、BasicMessageChannel、EventChannel 区别是什么?
  • 为什么连续事件要用 EventChannel?
  • Codec 做了什么?
  • PlatformView 为什么慢?
  • Flutter 调 iOS 的完整流程?
  • iOS 调 Flutter 的完整流程?
  • 线程怎么切?多 engine 怎么处理?

一句话结论:

Channel 本质上是基于 BinaryMessenger 的二进制消息通信;MethodChannel 适合请求响应,BasicMessageChannel 适合双向消息协议,EventChannel 适合 Native 连续事件流。PlatformView 慢是因为原生 View 被混入 Flutter 合成体系,带来合成、线程、触摸和生命周期同步成本。

1. 三种 Channel 的底层共同点

三者底层都依赖:

BinaryMessenger
-> Codec
-> platform message
-> Flutter Engine
-> Native embedder

它们的区别不是“谁更高级”,而是通信语义不同。

Channel通信模型Dart 侧抽象典型场景
MethodChannel方法调用 + 单次返回Future调 SDK、获取电量、打开原生页
BasicMessageChannel双向消息send / handler自定义协议、双向会话
EventChannelNative 连续事件流Stream定位、传感器、网络状态、扫码结果

2. MethodChannel

MethodChannel 像 RPC。

Dart:

const channel = MethodChannel('app/native');

final token = await channel.invokeMethod<String>('getToken');

iOS:

let channel = FlutterMethodChannel(
  name: "app/native",
  binaryMessenger: controller.binaryMessenger
)

channel.setMethodCallHandler { call, result in
  switch call.method {
  case "getToken":
    result("token-value")
  default:
    result(FlutterMethodNotImplemented)
  }
}

特点:

  • 方法名。
  • 参数。
  • 单次成功结果。
  • 单次错误结果。
  • notImplemented。

适合“一问一答”,不适合高频连续事件。

3. BasicMessageChannel

BasicMessageChannel 更像一条双向消息管道。

final channel = BasicMessageChannel<Object?>(
  'app/message',
  StandardMessageCodec(),
);

await channel.send({
  'type': 'ping',
  'payload': {'time': DateTime.now().millisecondsSinceEpoch},
});

特点:

  • 不强制 method name。
  • 双方都可以设置 message handler。
  • 可以自定义消息协议。
  • 适合长期会话或结构化消息。

例如:

{type: "page_visible", payload: {...}}
{type: "native_ready", payload: {...}}
{type: "sync_state", payload: {...}}

如果你的通信已经演化成大量 method name,且很多消息不是严格请求响应,可以考虑 BasicMessageChannel + 协议对象。

4. EventChannel

EventChannel 把 Native 事件源暴露成 Dart Stream。

Dart:

const channel = EventChannel('app/location');

final subscription = channel.receiveBroadcastStream().listen((event) {
  // location update
});

Native 侧实现:

onListen -> 开始监听原生事件
success(event) -> 多次推送事件
onCancel -> 停止监听原生事件

适合:

  • GPS。
  • 传感器。
  • 蓝牙扫描。
  • 网络变化。
  • 系统通知。
  • 扫码结果。
  • 下载进度。

5. 为什么需要 EventChannel?

如果用 MethodChannel 做定位,会变成轮询:

Flutter getLocation
Native return location
Flutter getLocation
Native return location
...

问题:

  • 浪费资源。
  • 实时性差。
  • 生命周期难关闭。
  • Native 无法自然主动推送。
  • 错误和结束事件不好表达。

EventChannel 更符合事件源:

Flutter listen
-> Native onListen
-> Native 多次 success(event)
-> Flutter cancel
-> Native onCancel

面试话术:

EventChannel 的价值是把 Native 连续回调转换成 Dart Stream,并用 listen/cancel 管理原生监听生命周期。它不是为了替代 MethodChannel,而是解决多次事件推送。

6. Codec 是什么?

Channel 最终传输的是二进制消息。Codec 负责对象和二进制之间的转换。

常见 Codec:

Codec作用
StandardMessageCodec默认消息编码,支持 null、bool、num、String、Uint8List、List、Map 等
StandardMethodCodec在 StandardMessageCodec 基础上编码 method call、success/error envelope
JSONMessageCodecJSON 编码,跨语言直观但类型表达弱
StringCodec字符串
BinaryCodec原始 ByteData

iOS 常见映射:

DartiOS
nullnil
bool/int/doubleNSNumber
StringNSString
Uint8ListFlutterStandardTypedData
ListNSArray
MapNSDictionary

7. Channel 为什么不适合大数据?

Channel 会涉及:

  • Dart 对象编码。
  • 二进制消息传输。
  • Native 解码。
  • 线程切换。
  • 对象复制。
  • Dart heap 和 Native heap 压力。

不要传:

  • 大图片 bytes。
  • 视频帧。
  • 高频大数组。
  • 大文件内容。
  • 高频日志流。

更推荐:

  • 传文件路径。
  • 传资源 id。
  • 传缓存 key。
  • 使用 Texture。
  • 使用 FFI。
  • 使用共享文件。

8. PlatformView 为什么慢?

Flutter 正常 UI 是自绘的:

Widget
-> RenderObject
-> Layer Tree
-> Engine
-> GPU

PlatformView 是把原生 UIView/Android View 嵌入 Flutter 页面。

慢的根源是:

原生 View 不属于 Flutter 自己的渲染树,却要和 Flutter 图层一起出现在同一屏幕。

成本包括:

  • 合成路径更复杂。
  • 原生主线程参与。
  • Flutter 和 Native 尺寸同步。
  • 触摸事件转发。
  • 焦点和输入法同步。
  • 生命周期同步。
  • clip/opacity/transform 支持受限或成本高。
  • 滚动列表中复用困难。

iOS PlatformView 使用原生 UIView 接入 view hierarchy。它不是普通 Flutter layer,所以在裁剪、透明、变换、叠加等场景更容易遇到性能和一致性问题。

9. PlatformView 使用建议

适合:

  • WebView。
  • Map。
  • Camera。
  • 视频播放器。
  • 必须依赖原生 View 的 SDK。

避免:

  • 长列表里大量 PlatformView。
  • 大面积透明和圆角裁剪。
  • 复杂 transform。
  • 和 Flutter 动画频繁叠加。
  • 可以 Flutter 自绘的普通 UI。

替代方案:

  • 大图/视频帧:Texture。
  • 简单 Native 能力:MethodChannel。
  • 高频计算:FFI 或 Isolate。
  • 复杂页面:尽量纯 Flutter 或纯 Native,减少混合层级。

10. Flutter 调 iOS 流程

以 MethodChannel 为例:

Dart invokeMethod
-> MethodChannel 用 StandardMethodCodec 编码 method + arguments
-> BinaryMessenger 发送 platform message
-> Engine 转给 iOS embedder
-> FlutterMethodChannel handler 收到 call
-> iOS 执行原生逻辑
-> result(success/error/notImplemented)
-> Codec 编码返回 envelope
-> Dart Future 完成

关键点:

  • channel name 必须一致。
  • codec 必须一致。
  • result 只能回调一次。
  • 原生耗时任务不要阻塞 main thread。
  • 多 engine 时 binaryMessenger 必须来自正确 engine/controller。

11. iOS 调 Flutter 流程

iOS 也可以主动调用 Dart。

iOS:

channel.invokeMethod(
  "nativeEvent",
  arguments: ["type": "loginExpired"]
) { result in
  // Dart 返回值
}

Dart:

const channel = MethodChannel('app/native');

void setupChannel() {
  channel.setMethodCallHandler((call) async {
    switch (call.method) {
      case 'nativeEvent':
        final args = call.arguments as Map<Object?, Object?>;
        return {'handled': true, 'args': args};
      default:
        throw MissingPluginException(call.method);
    }
  });
}

流程:

iOS invokeMethod
-> FlutterMethodChannel 编码
-> BinaryMessenger 发送
-> Engine 投递给 Dart
-> Dart handler 执行
-> Dart 返回结果
-> iOS completion 收到结果

12. iOS 调 Flutter 常见坑

  • Dart handler 还没注册,Native 已经调用。
  • FlutterEngine 还没启动。
  • 当前页面不是这个 engine。
  • 多 engine 下 channel 绑错 binaryMessenger。
  • Flutter 页面销毁后 Native 持有旧 channel。
  • iOS 在错误线程发消息。
  • Dart handler 做重 CPU 任务导致主 isolate 卡住。

解决:

  • engine 预热完成后再通信。
  • 建立 ready/handshake 机制。
  • 多 engine channel 封装到 engine owner。
  • 页面销毁时解绑 native callback。
  • 大数据不走 channel。

13. 线程怎么讲?

传统 Flutter 线程模型常讲四类任务:

Platform Thread:平台主线程,处理 UIKit/Android View 和插件入口
UI Thread:执行 Dart UI isolate,build/layout/paint,生成 layer tree
Raster Thread:栅格化 layer tree,提交 GPU
IO Thread:图片解码、资源加载等 Engine 内部任务

但要注意新版本变化:Flutter 官方 breaking change 文档说明,平台线程和 UI 线程在部分平台版本中发生了合并,以改善平台互操作成本。

所以更稳妥的表述是:

Flutter Engine 内部有平台、Dart/UI、Raster、IO 等任务分工。具体线程是否合并取决于 Flutter 版本和平台,但 Dart 主 Isolate 仍然是单线程事件循环。业务上必须避免阻塞 Dart 主 Isolate,也必须避免阻塞 iOS main thread。

14. Channel 回调线程原则

实践原则:

  • iOS UI API 必须在 main thread。
  • 原生耗时任务放后台队列。
  • 完成后按 Flutter/iOS 线程约束回调。
  • Dart 收到消息后仍在对应 isolate 事件循环处理。
  • 主 isolate 卡住时,Channel 回调也会排队。

伪流程:

Flutter call native
-> iOS main receives
-> dispatch background for heavy work
-> background completes
-> dispatch main if touching UIKit/channel result
-> result back to Dart

15. 多 Engine 通信怎么治理?

混合工程常见多 engine:

  • 预热 engine。
  • 独立业务 engine。
  • FlutterEngineGroup。
  • 多 Flutter 页面。

风险:

  • channel name 相同但 engine 不同。
  • Native 回调发给旧 engine。
  • Flutter 页面销毁但 engine 还在。
  • 登录态事件广播到错误页面。

治理建议:

  • channel 由 engine owner 创建和持有。
  • 不用全局单例 channel 绑定不明确 engine。
  • 协议层带 pageId/sessionId。
  • 页面销毁时注销 handler。
  • 统一 NativeFlutterBridge 管理生命周期。

16. 项目实战:网络请求为什么走 Native 壳?

我们项目的 Flutter 业务包没有使用 Dio,而是把网络请求委托给 Native 壳工程。

完整链路是:

Flutter 业务 ViewModel
-> RequestTools.get/post
-> HomeFlutterPluginAdapter.netGet/netPost
-> MethodChannel(home_flutter_plugin / beikesmartstorepad_flutter_plugin)
-> DecorationSteward: DWHomeFlutterPlugin / DWLifeFlutterPlugin
-> DWHomeNetworkService / BKBPNetworkAPI / DWCommonApiService
-> biz-gateway.home.ke.com

Flutter 侧封装在:

flutter_smartstorehome_package/lib/common/net/request_tools.dart
flutter_smartstorehome_package/lib/plugin/home_flutter_plugin.dart
flutter_smartstorehome_package/lib/plugin/home_flutter_plugin_adapter.dart
flutter_smartstorehome_package/lib/plugin/beikesmartstorepad_flutter_plugin.dart

Native iOS 壳落点在:

DecorationSteward/DecorationWorkflow/FlutterPlugin/DWHomeFlutterPlugin.m
DecorationSteward/DecorationWorkflow/FlutterPlugin/DWLifeFlutterPlugin.m
DecorationSteward/DecorationWorkflow/APIService/DWAPIService.m
DecorationSteward/DecorationWorkflow/New/Common/Network/DWCommonApiService.m

Flutter 发起请求时,只传业务维度的信息:

final map = await HomeFlutterPluginAdapter.netPost(
  url: url,
  params: params,
  body: body,
  bizId: bizId,
);

Native 插件收到 netPost 后,调用原生网络层:

DWHomeFlutterPlugin
-> DWHomeNetworkService.defaultService.postWithUrl

或者:

DWLifeFlutterPlugin
-> BKBPNetworkAPI.postWithUrl

最后 Native 把结果序列化成 JSON string 回传给 Flutter,Flutter 再解析成 BKBPResult

为什么不用 Flutter 自己发 HTTP?

因为这个项目是 Native 主导的混合工程。网络能力不是单纯的 HTTP 请求,而是壳工程统一治理的一组能力:

  • 登录态。
  • Token。
  • 角色。
  • 城市。
  • 网关环境。
  • Authorization 签名。
  • Cookie。
  • 设备标识。
  • User-Agent。
  • 登录过期处理。
  • 埋点。
  • 安全策略。

原生侧会统一写入这些 Header:

Authorization
Lianjia-Device-Id
Lianjia-Timestamp
Lianjia-Version
User-Agent
Lianjia-Im-Version
extension
Lianjia-Home-Role
Lianjia-Access-Token
authenticationCode
authenticationType
Cookie
Device-Info

如果 Flutter 使用 Dio,就必须在 Dart 层重新实现一套 header、签名、token、cookie、环境和错误处理。这不仅重复,而且容易和 Native 侧发生漂移。

这个设计的核心收益

复用 Native 登录态

Native 已经通过 DWUserManager 管理登录态和 token。Flutter 不需要再维护一套 token store,也不需要自己处理 token 刷新和退出登录。

这能避免:

Native 认为已登录
Flutter token 已过期
H5 cookie 还是旧值

这种混合工程里很常见的状态不一致。

复用网关签名和安全策略

原生侧有 Authorization 生成逻辑、防抓包开关、设备标识和内部网络 SDK。把这些放在 Native 层,可以减少安全逻辑在 Dart 层暴露,也避免 iOS、Android、Harmony、Flutter 多套实现。

统一环境切换

测试、预发、生产 host 在 Native 壳里统一控制。Flutter 业务包只传接口 path 或业务 url,不需要关心当前是哪个环境。

Native、H5、Flutter 请求口径一致

同一个 App 里可能同时存在:

Native 页面请求
Flutter 页面请求
H5 页面请求

网络通过 Native 壳统一后,Cookie、User-Agent、角色、城市、设备信息、登录过期逻辑都更容易保持一致。

Flutter 业务包更轻

Flutter 业务包只关注:

  • 接口地址。
  • 入参。
  • 返回数据结构。
  • 页面状态。

不需要关心公司网关、安全签名、角色 header、设备 header、Native 登录流。

这个设计的代价

这套设计也不是没有成本:

  • 每个请求都要过 MethodChannel,有编解码和跨端调用成本。
  • Flutter 侧不能直接使用 Dio 的 Interceptor、CancelToken、请求队列、下载进度等能力。
  • 请求取消较弱,页面 dispose 后 Native 请求不一定能同步取消。
  • Native 返回 JSON string,Flutter 再 jsonDecode,类型安全较弱。
  • 调试链路跨 Flutter、Plugin、Native 网络层,排查成本更高。
  • 单元测试需要 mock MethodChannel,不如 mock Dart repository 直接。
  • netGet / netPost 继续扩展下去,容易让 Channel 协议膨胀。

更合理的演进方向

不建议为了“Flutter 标准化”直接全量迁移 Dio。更合理的是把现有 Channel 网络协议做规范:

netRequest({
  requestId,
  method,
  url,
  params,
  body,
  bizId,
  timeout,
  extra
})

Native 返回统一结构:

{
  code,
  message,
  data,
  requestId,
  traceId,
  httpStatus,
  rawError
}

同时补:

  • cancelRequest(requestId)
  • Flutter/Native 双端日志串联。
  • requestId 透传。
  • 统一错误码映射。
  • MethodChannel mock 工具。

一句话总结:

我们项目不用 Dio,不是因为 Dio 能力不够,而是因为网络治理中心在 Native 壳。Flutter 通过 MethodChannel 委托 Native 发请求,换来登录态、签名、网关、Cookie、环境、安全和埋点的一致性。这是混合工程里的合理取舍。

17. 面试怎么回答?

可以这样答:

MethodChannel、BasicMessageChannel、EventChannel 底层都基于 BinaryMessenger 和 Codec。MethodChannel 是一次方法调用一次返回,适合 RPC;BasicMessageChannel 是双向消息通道,适合自定义协议;EventChannel 把 Native 连续事件暴露成 Dart Stream,适合定位、传感器、网络状态这类多次回调。

Codec 负责 Dart 对象和二进制消息之间的编码解码,StandardMessageCodec 支持常见 JSON-like 类型和 typed data,StandardMethodCodec 额外封装 method call 和 success/error envelope。Channel 不适合传大对象,大图、大文件、视频帧应该传路径/id,或用 Texture、FFI、文件共享。

PlatformView 慢是因为原生 View 被嵌进 Flutter 合成体系,不再是纯 Flutter layer,会带来合成、触摸、焦点、生命周期、线程和裁剪变换成本。WebView/Map/Camera 可以用,但不要在长列表里大量使用。

Flutter 调 iOS 是 Dart invokeMethod 经 Codec/BinaryMessenger/Engine 到 FlutterMethodChannel handler,再 result 回 Dart Future;iOS 调 Flutter 则是 invokeMethod 反向投递到 Dart handler。多 engine 场景要确保 binaryMessenger 绑定正确,线程上不要阻塞 Dart 主 isolate 和 iOS main thread。

我们项目的网络请求也是 MethodChannel 的典型工程化用法。Flutter 不直接使用 Dio,而是通过 home_flutter_plugin / beikesmartstorepad_flutter_pluginnetGetnetPost 把请求交给 Native 壳。这样可以复用 Native 已有的 token、签名、网关环境、Cookie、设备信息、登录过期和安全策略。缺点是请求取消、类型安全和调试链路会更复杂,所以后续应补 requestId、统一响应结构和 cancelRequest

参考资料