面试备战 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 | 自定义协议、双向会话 |
| EventChannel | Native 连续事件流 | 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 |
| JSONMessageCodec | JSON 编码,跨语言直观但类型表达弱 |
| StringCodec | 字符串 |
| BinaryCodec | 原始 ByteData |
iOS 常见映射:
| Dart | iOS |
|---|---|
| null | nil |
| bool/int/double | NSNumber |
| String | NSString |
| Uint8List | FlutterStandardTypedData |
| List | NSArray |
| Map | NSDictionary |
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_plugin的netGet、netPost把请求交给 Native 壳。这样可以复用 Native 已有的 token、签名、网关环境、Cookie、设备信息、登录过期和安全策略。缺点是请求取消、类型安全和调试链路会更复杂,所以后续应补requestId、统一响应结构和cancelRequest。
参考资料
- Flutter Platform channels: https://docs.flutter.dev/platform-integration/platform-channels
- MethodChannel API: https://api.flutter.dev/flutter/services/MethodChannel-class.html
- BasicMessageChannel API: https://api.flutter.dev/flutter/services/BasicMessageChannel-class.html
- EventChannel API: https://api.flutter.dev/flutter/services/EventChannel-class.html
- StandardMessageCodec API: https://api.flutter.dev/flutter/services/StandardMessageCodec-class.html
- iOS Platform Views: https://docs.flutter.dev/platform-integration/ios/platform-views
- Add Flutter screen to iOS: https://docs.flutter.dev/add-to-app/ios/add-flutter-screen
- Flutter thread merge breaking change: https://docs.flutter.dev/release/breaking-changes/macos-windows-merged-threads