Flutter鸿蒙化适配:json_rpc_2通信层迁移与双向交互方案
发布时间:2026/10/6 3:49:57 作者:尧图编辑部 阅读量:1,286

写 Flutter 鸿蒙化适配最让人头疼的往往不是 UI 能不能画出来而是通信层怎么打通。前阵子我把项目里的json_rpc_2库往鸿蒙端迁移折腾了几天踩了不少坑也把整个通信架构重新理了一遍。今天把这套适配方案整理出来从为什么选 JSON-RPC 2.0、怎么定义通讯准则到怎么在鸿蒙 Flutter 环境里实现双向交互一次性说清楚。这篇东西适合正在做 Flutter 鸿蒙化、或者想把原生与 Dart 侧通信梳理得更规范的开发者哪怕你还没碰过json_rpc_2按着这个思路走也能少走弯路。1. 为什么要在鸿蒙上做 json_rpc_2 适配1.1 json_rpc_2 是什么能解决什么问题json_rpc_2是 Dart 生态里一个非常成熟的 JSON-RPC 2.0 协议实现纯 Dart 编写不依赖任何平台通道。它提供了 Server、Client 两套完整的 API以及基于StreamChannel的传输层抽象。简单说它就是一套“定义好了请求、响应、通知、错误码、批量调用”的通讯框架你只需要把一个双向的字节流通道交给它剩下的协议细节它全包了。那这套东西在鸿蒙化场景里能解决什么问题核心是解决 Flutter 与鸿蒙原生之间通讯“太随意”的问题。平时我们用 MethodChannel无非是定一串字符串方法名两边写一堆 if-else 分发。方法少还好一旦超过几十个参数还带各种嵌套结构维护起来就是灾难。json_rpc_2把通讯规范变成协议本身的一部分请求里有 method、有 params、有 id响应里有 result 或者 error错误的语义也是标准化的。鸿蒙侧和 Flutter 侧都不用再各自维护一套“私有协议”而是共同遵守 JSON-RPC 2.0 这套公开标准。我在实际项目里最深的感觉是json_rpc_2不是来替代 MethodChannel 的而是给 MethodChannel 套上了一层“协议骨架”。通道还是那条通道但上面跑的不再是零散的方法调用而是结构化、可追踪、可校验的 RPC 消息。对于需要双向交互、需要超时语义、需要批量处理的场景这套骨架几乎是必需的。1.2 鸿蒙化适配的三个关键难点把纯 Dart 库往鸿蒙迁移理论上很顺因为json_rpc_2的依赖基本是stream_channel、synchronized这类同样纯 Dart 的包不涉及原生代码。但实际落地有三个坎几乎绕不过去。第一个坎是传输通道的建立。json_rpc_2自己不管消息怎么走它只要求你提供一个StreamChannel。但在鸿蒙的 Flutter 环境里Dart 侧没法直接监听鸿蒙原生发来的 TCP 或 Unix Domain Socket就算能涉及权限和沙箱限制也极其麻烦。最实际的做法是把MethodChannel和EventChannel包装成一个符合StreamChannel接口的适配层。这个方法可靠但需要注意鸿蒙原生侧的 MethodChannel 实现和 Android 上还不完全一样事件回调的线程模型有差异。第二个坎是双向交互的建模。常规思路是 Dart 侧的JsonRpcServer处理鸿蒙侧发来的请求反过来鸿蒙侧的请求怎么主动打给 Dart很多人在这里卡住因为 MethodChannel 的天然方向是“Dart 调用原生”或者“原生调用 Dart”但你想在一条逻辑连接上同时承载“Dart 请求-原生响应”和“原生请求-Dart 响应”两种模式就得在通道之上再造一层消息分发器。json_rpc_2的好处是它天然支持这种双工模型只要两端各跑一个 Server各持一个 Client就能互不干扰。第三个坎是异步时序的差异。鸿蒙 Flutter 引擎的事件循环、微任务队列与标准 Flutter 引擎有不少细节差异最典型的例子是 Future 回调的调度时机。json_rpc_2内部大量依赖 Stream、Future、StreamController一旦某个回调没有在预期的事件循环里被调度消息就会卡住。这个问题在开发机上很难复现真机跑一段时间才暴露。所以适配时日志追踪能力必须前置设计好。2. JSON-RPC 2.0 结构化通讯准则2.1 协议基础请求、通知、响应、错误码在动手写适配层之前必须把 JSON-RPC 2.0 的核心结构吃透。它一共只有四种消息形态请求Request、通知Notification、成功响应Success Response、错误响应Error Response。另外支持批量Batch调用一次发送一个数组。一个标准的请求对象长这样{ jsonrpc: 2.0, method: device.getInfo, params: {deviceId: abc123}, id: 1 }注意id是必须的、由请求方维护的唯一标识响应里会原样带回。如果不带id这就变成一个“通知”——发送方不期待任何响应接收方也不准回消息。这个机制特别适合鸿蒙侧向 Flutter 侧推送状态变化比如蓝牙连接状态、电量变化、页面生命周期切换直接发通知干净利落。响应同样必须带jsonrpc字段和id。成功响应用result携带数据错误响应用error携带一个结构化的错误对象{ jsonrpc: 2.0, error: { code: -32601, message: Method not found }, id: 1 }错误码是协议内定义的这一点特别适合用来统一鸿蒙和 Flutter 两侧的错误语义。标准错误码包括错误码含义说明-32700解析错误JSON 无法被解析-32600无效请求JSON 不是合法的请求对象-32601方法不存在调用的 method 在接收端没有注册-32602无效参数params 的结构与预期不匹配-32603内部错误处理器内部抛异常-32000 至 -32099服务端错误预留可自定义服务端异常建议业务错误码不要占用 -32000 以下的保留区间也不要试图覆盖标准错误码。我在项目里把业务错误统一映射到-32000 - code的区间这样从错误码本身就能区分是协议层错误还是业务层错误排查问题省下大量时间。2.2 通讯准则规范ID、命名、错误码、超时与日志通讯准则不是协议强制要求的但不定清楚后面一定乱。我把这套准则分成五块适配前就要定好。ID 生成规则。不要用简单的全局自增。鸿蒙侧如果有多个业务模块各自持有 Client自增 ID 会发生碰撞导致响应错配。我采用的方案是“模块前缀 单调递增序号”Dart 侧 ID 开头用F鸿蒙侧用N比如F-1001和N-1001。这样日志里看到 ID 前缀就知道消息是谁发起的排查链路的时候极其好用。方法命名规范。统一用模块.动作形式小写点分。例如device.getInfo、bluetooth.startScan、system.setOrientation。拒绝驼峰、拒绝带斜杠、拒绝没有模块前缀的裸方法名。这样在两端各自的方法注册表里按前缀就能快速分组。参数校验。接收端注册方法时必须写参数校验逻辑。json_rpc_2不帮你校验 params你拿到的是一个动态类型要自己处理。参数不合法直接返回-32602不要硬解析然后抛异常。否则你会看到一堆莫名其妙的-32603内部错误日志完全无法区分是代码 bug 还是调用方传参错误。超时约定。每个请求必须要有超时语义json_rpc_2的 Client 没有内建超时但 Dart 的Future.timeout()可以直接包住client.sendRequest()。超时时间建议按方法分档快速方法 2 秒慢方法如蓝牙扫描、文件读写5 秒超长任务用通知事件回调钳制。不要一个超时走天下。日志追踪。每条请求在 params 里注入一个traceId两端按traceId而不是按方法名打日志。这是我在实际踩坑里得出的最重要经验方法名只能告诉你“调用了什么”traceId才能告诉你“这一次调用是谁发起的、花了多久、在哪个环节断了”。3. 鸿蒙化适配实操3.1 环境准备与依赖替换先说环境。鸿蒙 Flutter 目前用的是 OpenHarmony 分支的引擎总体 API 与标准 Flutter 兼容但不是 100% 对齐。Dart 侧只要有dart:async、dart:convert这类基础库就能跑json_rpc_2完全满足这一条件。所以适配重头不在 Dart 侧而在原生侧的通道实现上。项目级准备工作如下在pubspec.yaml里添加依赖dependencies: json_rpc_2: ^3.0.7 stream_channel: ^2.1.1 synchronized: ^3.1.0如果原有代码直接引用了json_rpc_2的 WebSocket 或 HTTP 相关实现比如用package:web_socket_channel这部分建议先摘掉鸿蒙端直接用自研的 MethodChannel 适配层替代避免底层 socket 差异带来的不确定问题。构建链层面鸿蒙 Flutter 项目和普通 Android 项目构建流程不太一样但都是 Gradle 体系。这里要留意的是跑到真机之前先跑通模拟器因为模拟器上 MethodChannel 的调试日志更完整真机往往把早期崩溃吞掉。我建议在动手写代码前先把鸿蒙原生侧和 Flutter 侧的基础调用打通Flutter 调一个空方法、原生回一个空字符串、原生再调一个空方法、Flutter 回一句。这四步通了再谈 RPC 适配。很多问题其实出在通道初始化的时序上不是你 JSON-RPC 逻辑写错了。3.2 通道层封装MethodChannel 作为传输层这是整个适配的核心。json_rpc_2需要一个StreamChannel它内部通过这个通道收发字符串消息。我们要做的就是让 MethodChannel 看起来像一个 StreamChannel。思路是这样Dart 侧用一个StreamController接收“鸿蒙原生发来的消息”同时把“要发给鸿蒙原生的消息”通过 MethodChannel 的invokeMethod发出去。而鸿蒙侧原生代码通过 MethodChannel 的回调收到 Dart 侧消息并通过result.success()回传再把要发给 Dart 的消息通过MethodChannel的invokeMethod主动吐给 Flutter。说白了两件事向外复用 invokeMethod 这一条路向内复用 EventChannel 那一条路。两边各有一个发射管道和一个接收管道拼起来就是一个逻辑上的双向 StreamChannel。Dart 侧代码大概长这样这是关键路径我贴的是精简可运行版import dart:async; import package:flutter/services.dart; import package:stream_channel/stream_channel.dart; class MethodChannelStreamChannel extends StreamChannelMixinString { final MethodChannel _ethodChannel; final StreamControllerString _controller StreamControllerString.broadcast(); MethodChannelStreamChannel(this._methodChannel) { _methodChannel.setMethodCallHandler((call) async { if (call.method onRpcMessage) { _controller.add(call.arguments as String); } return null; }); } override StreamString get stream _controller.stream; override Futurevoid add(String data) { return _methodChannel .invokeMethod(sendRpcMessage, data) .then((_) {}); } override Futurevoid close() async { await _controller.close(); } }鸿蒙侧则是在主 Ability / 组件里注册 MethodChannel 的 handler监听sendRpcMessage并处理onRpcMessage的调用。这里天然就是双向的原生侧调用invokeMethod(onRpcMessage, jsonString)Dart 侧就会收到Dart 侧调用invokeMethod(sendRpcMessage, jsonString)原生侧就会收到。有个细节坑要提前说千万不要用同一个 MethodChannel 名字注册两套 handler。鸿蒙 Flutter 的 MethodChannel 一套名字只能绑定一个 handler否则后注册的会覆盖先注册的无声无息。我建议通道名分开com.example.rpc/in命名接收通道、com.example.rpc/out命名发送通道实际还是一个 MethodChannel 对象只是 method 名区分。上面代码里我用的就是单通道双 method 的做法更简单也更容易排查。3.3 Server 与 Client 双向交互实现通道层准备好了接下来就是真正的双向交互。目标是两个能力Dart 侧注册方法鸿蒙侧通过 RPC 调用。鸿蒙侧注册方法Dart 侧通过 RPC 调用。先说 Dart 侧作为 Server 怎么做。import package:json_rpc_2/json_rpc_2.dart as json_rpc; final rpcServer json_rpc.Server(channel); rpcServer.registerMethod(device.getInfo, (params) { final deviceId params[deviceId].asString; if (deviceId.isEmpty) { throw json_rpc.InvalidParamsException(deviceId is required); } return {name: Harmony Device, id: deviceId}; }); rpcServer.listen();json_rpc_2的registerMethod自带参数封装params是一个Parameters对象可以用.asString、.asList、.asMap取类型化的值。传参错误会抛InvalidParamsException框架自动转成-32602错误码不用自己拼错误对象。鸿蒙侧作为 Client 发起请求以 ArkTS 为例先把要发的请求 JSON 序列化通过invokeMethod(sendRpcMessage, jsonString)发出。响应会走setMethodCallHandler里的onRpcMessage回来再按id匹配到具体请求。这里最大的坑来了不是每个响应都能立即回来。Dart 侧的方法 handler 如果要等一个真正的异步操作比如蓝牙扫描、读文件、调鸿蒙 APIjson_rpc_2的 Server 是支持异步方法的registerMethod 的函数返回Future即可。但响应会延迟返回原生侧必须做好“发请求 - 记录回调 - 响应回来再触发回调”的映射表而不是简单地在setMethodCallHandler里同步 return。反过来鸿蒙侧作为 Server、Dart 侧作为 Client 也是同理。你只要在两边各建一个 Server 实例、各建一个 Client 实例分别挂到同一个双向通道上双向交互自然就成立了。这里有个性能优化点不要为每对调用新建 Server/Client要在应用启动时创建一次全程复用。json_rpc_2的 Server 在重复注册同名方法时会直接抛异常我之前在这里栽过跟头启动时注册一次后面别再碰注册表。双向交互的完整消息流大概是这样消息方向消息类型method 示例说明鸿蒙 → FlutterRequestdevice.getInfo鸿蒙主动查询 Flutter 侧状态Flutter → 鸿蒙Response(无)携带 result 返回Flutter → 鸿蒙Notificationapp.pageChangedFlutter 通知鸿蒙页面切换鸿蒙 → FlutterRequestbluetooth.startScanFlutter 请求鸿蒙启动蓝牙扫描鸿蒙 → FlutterNotificationbluetooth.deviceFound鸿蒙持续推送发现的设备这个表格里的场景是我实际做过的Flutter 页面要监听鸿蒙蓝牙扫描结果原生侧每扫到一个设备就发一条 NotificationDart 侧订阅事件流直接更新 UI不需要 Flutter 反复轮询。这就是用 JSON-RPC 2.0 做鸿蒙级双向交互的典型姿势。4. 踩坑记录与排查技巧实录4.1 常见错误与排查方法速查表实际适配过程中我遇到的问题远不止“代码写错”更多的是鸿蒙 Flutter 引擎的底层行为差异。下面这些是我整理的最有代表性的坑。问题现象根因排查方法PlatformException(channel_error)MethodChannel 未在原生侧注册或注册时机晚于 Dart 侧首次调用启动初始化顺序调整原生侧先注册通道再加载 Flutter 页面消息发出去了但对方没收到EventChannel 的监听还没建立消息已经发出鸿蒙侧发送通知前先等待 Dart 侧回执“通道就绪”响应错配/乱序多个 Client 共用了同一个 ID 序列检查 ID 生成规则加模块前缀日志对照JSON 解析异常鸿蒙侧发送的不是合法 JSON或字符串里混入了 UTF-8 BOM在 Dart 侧 catch FormatException把原始字符串打出来异步回调丢失鸿蒙侧setMethodCallHandler的 async 回调在页面消失后被回收确保通道实例生命周期与页面/Ability 一致不要用局部变量持有 handler排查 JSON-RPC 问题最重要的手段就是日志先行。我在 Dart 侧和鸿蒙侧都加了一层统一的日志钩子每收到一条消息打[RPC RECV] json每发出一条消息打[RPC SEND] json。一开始会觉得刷屏但定位问题的时候没有这些日志你就是在瞎猜。有一类问题特别隐蔽鸿蒙侧的invokeMethod在某些场景下不会走then而是走catchError。原本在 Android 上能吞掉的错误在鸿蒙上会直接抛出来。比如原生侧 handler 里抛了一个业务异常Dart 侧的Future可能会变成 error 状态导致整个消息链路中断。我的处理办法是鸿蒙侧 handler 里包一层统一的 try-catch任何异常都转成 JSON-RPC 错误对象返回绝不让异常穿透到通道层。4.2 性能与稳定性优化建议json_rpc_2本身不算快因为它内部是纯 Dart 解析 Stream 分发。在鸿蒙 Flutter 引擎上性能瓶颈会更明显尤其是高频小消息场景比如蓝牙设备每秒上报 20 次状态。这时批量机制就会派上用场JSON-RPC 2.0 原生支持批量请求数组里放多个请求一次通道调用处理完。批量后通道调用次数可以下降一个数量级。另一个优化点是通道消息大小。MethodChannel 内部传输有消息体大小限制虽然鸿蒙侧的具体阈值我没能完整测出但经验上是超过 256KB 就有可能断连。大对象不要走 RPC 通道改走临时文件或私有目录RPC 只传路径和签名这是我在文件传输场景下的明确结论。还有一个稳定性建议是心跳保活。鸿蒙系统有内存回收策略长时间无消息的双向通道存在被系统回收的风险。我在两端的 Client 里各开了一个定时器每 15 秒互发一条ping通知。这个通知不带 id不期待响应纯保活。加上之后渡渡过长时间挂后台再回前台的断线概率明显下降。内存泄漏也是个大坑尤其是 StreamController 忘记 close。MethodChannelStreamChannel.close()里必须清理setMethodCallHandler否则页面销毁后 handler 还会被调用造成泄漏。我建议在 Flutter 页面dispose时显式调用两端通道的 close并把 RPC Server 和 Client 置空。关于性能测试我个人的建议是不要只看单次调用的耗时要压“混合负载”。我在测试里同时跑了高频通知、批量请求、低速大对象传输三种负载才暴露出消息排队互相阻塞的问题。优化的手段是在 Dart 侧给不同优先级的方法分派到不同的 Server 实例上相当于给 RPC 通道做了流量隔离。通用场景不需要但如果你也在鸿蒙上做实时性要求高的功能这个方案值得参考。最后再分享一个我自己体会最深的点json_rpc_2这套协议让两端沟通的“语言”统一了真正难的不是实现 JSON-RPC而是憋住“这里加个临时字段就行了”的冲动。通讯准则一旦定下来就不要轻易改哪怕改一个字段名也要走双端版本协商。我后面再做鸿蒙原生与 Flutter 的通信架构一定会把 JSON-RPC 2.0 作为默认选项这不是因为它花哨而是它让我在凌晨三点排查线上问题时依然能靠日志和 ID 迅速定位问题。