
上手鸿蒙适配这事最早是因为我们边缘端那批 Flutter 客户端已经跑了大半年Android、iOS 都稳了突然要接一批鸿蒙开发板。原本架构里这部分设备之间是靠 REST 轮询加中央服务器中转的但边缘场景网络差、节点又多轮询的弊端越来越明显。后来我把通信层换成了 jerelo——一个基于 Dart 的 JSON-RPC 2.0 组件让设备之间直接“远程过程调用”不再什么都往中心服务器捅。这次把 jerelo 适配到 HarmonyOS 上中间踩了不少坑但也把整套边缘端分布式协同架构理顺了。这篇就当是一次完整复盘给正在做 Flutter 鸿蒙跨端、或者想用 JSON-RPC 代替 REST 做设备间通讯的朋友做个参考。1. 项目整体设计与选型思路1.1 为什么边缘端协同选了 Flutter jerelo边缘端和普通 App 不一样设备数量多、网络抖动大、机器配置参差不齐。我们这边管理的是几十台终端有的在产线有的在门店需要互相调用能力比如 A 设备请求 B 设备执行一次视频转码、C 设备上报聚合数据、D 设备远程触发一次模型推理。如果用传统 App 的做法所有请求都走中心服务器那边缘节点之间一次协作就要绕一大圈延迟高不说服务器一挂整个链路就瘫了。选 Flutter 是因为团队已经在这套代码上沉淀了完整的业务组件跨 Android/iOS 一直是同一套代码。鸿蒙出现后社区维护的鸿蒙化 Flutter SDK 已经能跑起不少生产项目所以客户端层继续用 Flutter 是性价比最高的选择。问题只剩下通信层。一开始我也考虑过 gRPC但边缘端设备上要引入 protobuf 编译链、维护 .proto 文件对轻量化场景来说太重了。REST 又太“面向资源”两个设备之间要执行一个动作得先定义 URL、请求方法、状态码还得处理各种中间态。我们真正需要的是“在远端设备上执行一个函数”这就是远程过程调用RPC。JSON-RPC 2.0 协议本身只有一页纸简单到几个方法就能实现配合 Flutter 生态里的 Dart 原生 JSON 能力非常契合。jerelo 这个组件就是在这种需求下进来的。它把 JSON-RPC 2.0 的协议层、连接层、调用调度层封装好了我只需要关心业务方法注册和调用。相比自己从零写一套轮子它省掉了大量边界处理。1.2 JSON-RPC 2.0 在边缘端比 REST 强在哪把 REST 和 JSON-RPC 放在一起对比可能有人会觉得 REST 才是主流但要看场景。边缘设备间通信的特点是短小、频繁、双向、状态变化快JSON-RPC 天然匹配这些特点。对比项RESTJSON-RPC 2.0请求语义资源 HTTP 动词方法名 参数连接利用通常短连接或 Keep-Alive 串行一条长连接并发处理多个请求响应结构需自定义错误结构统一 result / error 结构通知消息要么请求要么响应无单向有 notification不需要响应批量调用需额外设计原生支持 batch 请求协议复杂度低但业务映射复杂低语义更直接这里面最有用的是“通知消息”。设备间心跳、事件上报、状态推送这类单向消息REST 也得走一次完整请求RPC 里一个notification就解决了省掉一半包体积和响应处理逻辑。另一个很关键的点是长连接复用。边缘端设备经常处在弱网环境频繁建连非常不可靠。JSON-RPC 可以跑在 WebSocket、TCP 甚至自定义传输层上。我们最终选了 WebSocket 做主要传输兼容性最好跨平台实现最省事鸿蒙的 WebSocket 能力也很完整。1.3 jerelo 组件在整体架构里的定位我画过一张分层图jerelo 处在数据传输的中间层上面是业务方法下面是物理连接。具体拆开是这样连接层负责 WebSocket/TCP 的建立、断开、重连、心跳这一层对鸿蒙来说是最需要注意的协议层负责 JSON-RPC 2.0 报文的编码、解码、ID 映射、错误包装调度层负责把远端请求路由到本地注册的方法并把本地调用的结果发送回去。jereelo 把这套逻辑抽象成Jerelo入口类代码里只需要初始化一次。它不关心具体业务方法内部怎么实现只解决消息可靠到达、结果正确返回的问题。所以在鸿蒙适配中主要工作都在连接层和协议层的平台差异处理上。2. 鸿蒙适配前的环境准备与工程改造2.1 搭建鸿蒙化 Flutter SDK 开发环境这里先说明一下鸿蒙的 Flutter 支持目前来自社区维护的鸿蒙化分支和官方 Flutter 主干不是完全同步的。所以第一步不是直接flutter doctor而是先把本机的 Flutter 版本切到一个支持鸿蒙构建的 SDK 上。我本地用 fvm 管理多版本 Flutter单独拉了一个鸿蒙分支项目里加.fvmrc锁定版本。具体流程是这样的fvm init --name ohos-flutter --version ohos-3.7.12 fvm use ohos-flutter然后把 HarmonyOS 的 SDK 目录配置到环境变量里确保 DevEco Studio 的命令行工具hvigorw能直接识别。安装完 DevEco Studio 后会自带 HarmonyOS SDK路径通常在~/Library/Huawei/Sdk或C:\Huawei\Sdk。建议把hvigorw也加到 PATH后面构建 HAP 包会用到。这里有一个容易踩的坑鸿蒙 Flutter 分支需要匹配对应的 HarmonyOS SDK 版本如果 SDK 太新或太旧构建时会报一些莫名其妙的符号错误。建议先跑一下官方示例工程能跑通再进入到自己的项目。2.2 把已有 Flutter 工程改造成支持鸿蒙有了鸿蒙化 Flutter SDK下一步是把现有项目加上鸿蒙平台目录。理论上在项目根目录执行flutter create --platformsohos .这会生成ohos目录里面是鸿蒙工程骨架包括AppScope、entry、hvigorfile.ts这些文件。如果是老项目得注意flutter pub get之后ohos目录里可能没有自动生成完整配置文件需要手动检查ohos/entry/src/main/module.json5。模块配置文件里最核心的是网络权限。边缘端设备间通信必须申请网络访问权限否则 WebSocket 连接会被系统拦掉。在module.json5的requestPermissions里加上{ module: { requestPermissions: [ { name: ohos.permission.INTERNET } ] } }如果你还需要在局域网内做节点发现比如广播自己的服务地址可能还需要ohos.permission.DISTRIBUTED_DATASYNC这类分布式软总线相关权限。但从实际经验看边缘端跨设备协调尽量走明确地址连接不要过度依赖系统发现能力权限和兼容性问题会少很多。2.3 集成 jerelo 依赖并完成初始化环境准备好后在pubspec.yaml里加依赖dependencies: flutter: sdk: flutter jerelo: ^1.2.0然后flutter pub get。jerelo 本身是纯 Dart 实现不依赖原生插件这点在鸿蒙适配中省了很多事只要 Socket 和 WebSocket 能力可用它就能跑。初始化代码类似这样final rpc Jerelo( transport: WebSocketTransport( Uri.parse(ws://192.168.1.100:8080/rpc), ), codec: JsonRpcCodec(), maxPending: 128, ); await rpc.connect();这里有几个参数值得说transport可替换成 TCP、WebSocket、甚至自定义的串口通信边缘端有些设备没有 WiFi用有线串口也能跑 RPCmaxPending同一时刻未返回的请求数量上限。边缘设备内存不大限制并发能防止高负载下请求堆积codec编解码器默认使用 Dart 的dart:convert后续可以通过自定义 codec 做压缩或二进制优化。到现在为止鸿蒙适配的“壳”已经完成了工程能构建、网络权限有了、RPC 组件也能初始化。接下来要深入看 JSON-RPC 2.0 通讯本身怎么在鸿蒙上稳定跑。3. JSON-RPC 2.0 通讯核心实现与性能细节3.1 消息结构与请求 ID 生成策略JSON-RPC 2.0 协议本身很简单请求消息长这样{ jsonrpc: 2.0, method: device.transcode, params: { source: /data/video/01.mp4, format: h265 }, id: node-a-1024 }响应消息要么返回result要么返回error{ jsonrpc: 2.0, result: {taskId: 12345}, id: node-a-1024 }{ jsonrpc: 2.0, error: { code: -32601, message: Method not found }, id: node-a-1024 }这里我特别想提醒id的生成策略。很多人图省事用一个自增整数但边缘端同时有多个节点互相调用时如果有节点恰好也生成了一样的id响应就会串掉。我在生产环境里一直用“节点 ID 自增序列号”拼接字符串比如node-a-1024这样即使两个设备并发调用ID 冲突概率也几乎为零。int _seq 0; String nextRequestId(String nodeId) { _seq; return $nodeId-$_seq; }除了请求和响应还有一类 notification 消息它没有id接收方不需要返回任何东西。这类消息适合做心跳、事件通知。在边缘端设备状态变更频率高用 notification 推送能让对端及时感知又不会加重响应负担。3.2 连接管理与并发请求控制长连接不是建立完就不管了核心问题在于如何管理“未完成请求”和“并发上限”。我在 jerelo 内部维护了一个pending映射把请求id对应到一个Completerclass PendingRequest { final Completerdynamic completer; final DateTime deadline; } final _pending String, PendingRequest{};当方法调用发出去就先往这个表里塞一条记录收到响应后根据id把对应Completer的值填上再删掉记录。如果长时间没有响应就主动取消并返回超时错误。边缘设备的网络不可靠所以还要给并发设置闸门。我在设计时给maxPending设成了 128超过这个数的新请求要么排队要么直接拒绝。否则弱网环境下一个连不上对端的节点会在短时间内发上百个请求内存被塞满整台设备就卡死了。另外不要忽略批量请求。JSON-RPC 2.0 支持一次发送一个数组的请求[ {jsonrpc: 2.0, method: led.set, params: {on: true}, id: 1}, {jsonrpc: 2.0, method: motor.stop, params: {}, id: 2} ]边缘端场景里一个“动作”经常要联动多个设备的状态用批量请求可以减少交互轮次。但批量请求并非越大越好我实测单个 batch 超过 16 个请求低端设备处理时会明显卡顿所以需要通过配置限制单批数量比如分块发送。3.3 性能调优编解码、背压与线程调度鸿蒙设备性能跨度很大有的开发板 CPU 较弱JSON 编解码反而成了瓶颈。JSON-RPC 是文本协议再怎么优化也没有二进制快但我们可以在几个地方做工程优化。第一个是编解码策略。默认jsonDecode/jsonEncode在主 Isolate 里执行如果 RPC 消息很频繁UI 会掉帧。我的做法是把消息解析放到后台 isolate 里final parsed await compute(parseJson, rawMessage); dynamic parseJson(String raw) { return jsonDecode(raw); }解析完成后再通过SendPort把对象传回主 Isolate 做业务分发。这样 UI isolate 的职责只剩状态更新和轻量调度。第二是背压。RPC 走长连接如果发送端短时间内发出大量 notification接收端可能处理不过来。我在 jerelo 的发送队列里加入了一个水位线如果待发送的字节数超过阈值就暂停后续发送等队列排空再继续。这就类似水管里的减压阀宁可短暂阻塞也不能把对端冲垮。第三是方法路由。边缘端各业务方法调用很频繁如果每个方法都走MapString, Function的反射式查找性能其实一般。在 Dart 里更好的做法是用switch分支或者把方法名映射为整数 id减少字符串比较。比如device.transcode映射成1001路由时直接匹配数字速度能提升一个数量级。4. 边缘端分布式协同架构中的 jerelo 落地4.1 节点注册、发现与能力列表有了 RPC 通道下一步就是让设备之间互相知道“你能干什么、你在哪里”。我们用了一个轻量级的中心协调节点但这不是说所有调用都要经过中心。协调节点只负责注册和发现真正的数据流走边缘设备之间的点对点连接。每个节点启动时会向协调节点注册自己的地址和能力列表{ jsonrpc: 2.0, method: registry.register, params: { nodeId: node-a, endpoint: ws://192.168.1.101:8080/rpc, capabilities: [video.transcode, model.inference] }, id: node-a-reg-1 }其他节点需要调用某个能力时先查协调节点拿到目标端地址再直接建立点对点连接。这样做的好处是中心节点不参与高频业务数据流压力小很多即使中心节点短暂抖动已建立的边缘连接也不受影响。鸿蒙上如果要做纯本地发现可以用 mDNS但我建议尽量走“中心注册 点对点连接”模式。原因很简单mDNS 在跨网段、多子网环境下基本不可用边缘端网络环境本来就复杂别给自己挖坑。4.2 方法注册与路由设计在服务端也就是被调用的设备上jerelo 提供了方法注册接口。最基础的做法是rpc.register(video.transcode, (params) async { final source params[source]; final format params[format]; // 执行转码任务 return {taskId: 12345}; });这个方法处理器接收params返回一个能被 JSON 序列化的对象或者抛出一个异常。如果异常是JereloException它会自动转成 JSON-RPC error 响应如果抛出的是普通异常建议在顶层捕获并封装成-32603 Internal error。方法命名最好带上命名空间比如device.led.setBrightness、device.motor.stop、task.queryStatus。边缘端有大量设备单一扁平命名很容易冲突。参数校验也很重要。JSON-RPC 2.0 的params可能是数组也可能是对象。我在每个 handler 里先用一个简单的 schema 校验rpc.register(device.led.set, (params) { final on params is Map ? params[on] : null; if (on is! bool) { throw JereloException.invalidParams(on must be bool); } // ... });这样出问题的时候错误信息至少是明确的排查效率高很多。4.3 心跳保活、断线重连与状态补偿边缘端连接稳定性再怎么做也不过分。我们采用的策略是每个节点每隔 15 秒发送一条heartbeatnotification{ jsonrpc: 2.0, method: rpc.heartbeat, params: { ts: 1735699200000 } }接收方收到心跳后更新对端状态如果超过 45 秒没有收到任何消息就判定对端离线。断线重连不能直接死循环重试否则几十台设备同时掉线恢复网络后会形成“重连风暴”。我们用指数退避加随机抖动int retryDelay 1; while (!connected) { await Future.delayed(Duration(seconds: retryDelay)); retryDelay min(retryDelay * 2, 30) Random().nextInt(3); }这是个很经典但很实用的方案。第一次重连等 1 秒第二次等 2 秒第四次就是 8 秒最高到 30 秒封顶。随机抖动避免所有设备步调一致同时又不会让用户觉得完全没反应。除了重连还要考虑“状态补偿”。边缘端一次操作经常涉及多个设备假设设备 A 调起设备 B 的转码任务成功后 B 又需要把结果推送给 C这时候如果连接断了整个状态就悬空了。我的做法是在关键请求里加入业务层幂等 ID接收方记录最近处理过的请求 ID重复调用直接返回上一次结果。这跟 TCP 的重传机制一个道理应用层必须自己搞定去重。5. 常见问题与排查技巧实录5.1 鸿蒙上连接失败或秒断先查权限和构建产物鸿蒙上最常见的表现是Flutter 端代码在 Android 上跑得好好的一跑到鸿蒙rpc.connect()要么直接抛错要么握手成功但立刻断开。排查下来大部分情况都在权限和网络配置上。先确认module.json5里有没有ohos.permission.INTERNET。很多从旧工程复制过来的项目只改了包名和应用名忘了迁移权限列表。权限缺失时连接会静默失败不会弹提示。另一个隐藏问题是长连接被系统挂起。鸿蒙对后台网络有限制如果 App 退到后台WebSocket 可能被挂起切回前台才恢复。所以边缘端设备如果需要在锁屏或后台继续提供 RPC 服务得做成常驻前台应用或者在鸿蒙侧申请长任务权限。5.2 RPC 收发消息时 UI 掉帧问题在 isolate有一段时间我在鸿蒙开发板上跑应用设备轮询状态时就感觉列表刷新特别卡。后来用性能分析工具一看UI isolate 主线程被jsonDecode占了很大比例。原因是 RPC 消息进来之后直接在默认的webSocket.onMessage回调里做了 JSON 解析这个回调跑在主 isolate 上。解决方式就是我前面提到的compute后台解析。这里有个细节频繁用compute也有成本更好的方式是长期运行一个后台 isolate通过ReceivePort接收原始消息解析完再SendPort回传。如果消息频率不高用compute就够。5.3 低端设备上 JSON-RPC 响应慢可能是序列化对象太大开发板性能不够时一个包含大字符串的result从产生到完全序列化可能耗时几百毫秒。这种情况别硬扛应该设计成“RPC 传元数据 文件通道传数据”。比如设备 A 请求设备 B 生成一份报表B 的响应里只要返回报表 ID、大小、存储路径A 再通过 HTTP 或文件共享协议拉取文件。JSON-RPC 这类方法调用适合控制流不适合传输大数据块。这是一个架构层面的取舍而不是协议层面的缺陷。我踩过最深的坑是试图用 RPC 传一帧 4K 图像结果内存和序列化时间都爆了后来改成 RPC 传任务状态、图像走共享存储才稳定下来。5.4 问题排查速查表现象可能原因处理方法连接建立后无任何响应请求 ID 冲突或响应与请求未匹配使用带节点前缀的字符串 ID避免自增整数冲突调用方法返回 -32601handler 未注册或方法名拼写错误打印节点上已注册方法列表确认命名空间返回 -32603handler 内部抛了未被捕获的异常打开错误日志把异常连带 stack 打出来设备频繁断线心跳超时设置过短或网络本身不稳定调整心跳间隔和超时阈值增加指数退避重连鸿蒙设备发热、耗电快心跳/通知过于频繁或后台未休眠降低心跳频率非关键通知合并成批量推送批量请求执行异常batch 里某个请求报错影响后续解析单发逐个调试确认是协议问题还是业务逻辑问题这些坑每个单拿出来都不复杂但组合在一起足够让人排查好几天。尤其是鸿蒙生态还在快速迭代官方文档和社区案例都不算多很多问题只能靠日志和二分法定位。建议在实际项目里做好日志埋点把 RPC 的收发包原始 JSON 记录成文件出问题时能直接看到双方交换了什么内容比瞎猜高效得多。一点实践后的个人体会做过这轮适配之后我对 Flutter 跨端的能力又高看了一眼。jerelo 本身是纯 Dart 组件鸿蒙上的网络栈和 Android 有差异但 RPC 的核心调度逻辑完全不用改改动都收敛在连接层。这反过来验证了一个技术选型原则底层传输可以替换应用层协议最好稳定。只要 JSON-RPC 2.0 这套消息语义不变未来就算鸿蒙 SDK 再怎么升级迁移成本也都在可控范围。如果你也在做边缘端 Flutter 开发我建议先把 RPC 通讯基础设施打稳再往上堆业务。先把心跳、重连、并发限制、日志这些基本功做好后面接多少设备都不会慌。