Flutter鸿蒙适配实战:at_server_status插件改造全记录
发布时间:2026/10/1 11:22:40来源:尧图网络
做 Flutter 鸿蒙适配真正让人头皮发麻的往往不是 Flutter 引擎本身而是那一堆藏在纯 Dart 层下面的平台插件。前两天我把at_server_status这个库迁移到鸿蒙工程里前后折腾了近两天。这库看着不起眼却是protocol去中心化身份体系里非常关键的一环它负责实时感知 atServer 的在线状态、响应延迟、SSH 密钥配对情况以及鉴权链路的健康度。应用侧只有拿到这些透明、实时的状态才能在用户毫无感知的情况下完成自动重连、密钥轮换和异常告警。这篇文章不打算讲空话就记录我从依赖审计、插件壳创建、平台通道设计到最终跑通flutter run -d hdc的完整过程顺手把那些网上搜半天也搜不到的问题列出来。1. 先搞明白 at_server_status 到底在做什么1.1 去中心化身份服务器状态感知是什么protocol这套体系里每个用户都拥有一台被称为 atServer 的个人数据服务器用户的身份、备书、共享数据都存放在这里。既然是个人服务器它的运行状态就不是云厂商那种“可用性 99.99%”可以一概而论的可能你家里的路由器重启了可能服务器所在机房出口被封了也可能 SSH 密钥因为换机重新生成导致握手失败。这时候客户端如果没有一个清晰的状态感知层用户体验就会变成“莫名其妙连不上、也看不到哪里出了问题”。at_server_status解决的问题就是这个。它不是简单的 ping 一下 IP而是从三个层面去做状态判断第一是网络连通性也就是 TCP 层能不能连上第二是 atServer 协议层的响应是否正常比如能否返回开头握手响应第三是鉴权链路的可用性通常会校验 SSH 密钥对与服务器本地密钥是否仍然匹配。这三个层面组合在一起才算是“感知到了真实状态”。在鸿蒙上做这件事麻烦点不在于 Dart 层的逻辑而在于你没法假设底层网络栈和密钥存储行为跟 Android 完全一致。HarmonyOS NEXT 把 AOSP 那一套剥离之后很多原本在 Android 上“顺手能用”的东西到了鸿蒙上就得换成系统原生能力去补位。1.2 原版库的依赖和平台通道原版的at_server_status从代码结构看是一个偏纯 Dart 的库核心逻辑集中在状态机、超时控制和结果归集上。网络请求部分主要依赖http和web_socket_channelSSH 密钥操作则借助ssh_key和asn1lib完成。理论上这种纯 Dart 依赖是可以直接跑在 OpenHarmony 的 Flutter 运行时上的。但真跑到真机上问题就暴露了鸿蒙系统的网络策略、DNS 解析行为、安全存储接口跟 Android/iOS 有差异尤其当你需要做“透明”状态监控时不能简单依赖 HTTP 层头进行探测而要拿到更底层的 socket 连接状态和握手耗时。所以我在适配时做了一个很克制的决定Dart 层尽量保留原本的 API只新增一个极薄的 PlatformChannel用于获取原生网络探测能力和密钥存储兜底。为什么说“克制”因为很多团队一做适配就忍不住把整个库重写成原生逻辑这完全走偏了。at_server_status的核心价值在状态机逻辑和业务语义原生层只需要提供“连接是否可达”“加密握手耗时”这类底层原子能力。保留 Dart 层还带来一个额外好处后续鸿蒙 Flutter 引擎升级或者 OpenHarmony 分支底层实现变化Dart 逻辑不需要再动。2. 鸿蒙化适配前的环境与策略准备2.1 Flutter on OpenHarmony 的落地方案目前跑鸿蒙的 Flutter 方案不是官方 Flutter SDK 直接支持而是 OpenHarmony SIG 维护的flutter_flutter仓库。这块环境搭建有几个容易踩坑的点我按顺序说。首先安装鸿蒙 Flutter SDK。这里注意别用 Flutter 官方渠道的flutter命令去执行flutter doctor否则你永远看不到ohos平台。正确做法是拉取flutter_flutter的分支代码切到oh-xxx对应版本然后把bin目录加入 PATH。其次鸿蒙侧需要安装 DevEco Studio并且 SDK 版本要与 Flutter 分支要求的 ohos API 版本对齐。我用的是 API 12 的 SDK对应 Flutter 3.22 的分支整体兼容性是目前比较稳的组合。如果你直接上 API 14 或者更新的 DevEco有些中间产物路径会变插件编译时容易找不到ohos-sdk。最后在pubspec.yaml里不需要额外标记平台生成ohos目录需要执行flutter create --templatemodule --platformsohos .。这个命令在老版本 Flutter 分支里可能不存在需要确认你拉的分支是否已经内置了 ohos 模板。如果没内置就手动创建ohos目录再写oh-package.json5跟标准 OpenHarmony 工程结构对齐。2.2 依赖审计与替代方案选择开始改造之前先把at_server_status的依赖树拉出来看一遍dependencies: at_server_status: path: packages/at_server_status我实际用flutter pub deps --stylecompact看到的依赖包括http、web_socket_channel、ssh_key、asn1lib、meta。这些包里面http和web_socket_channel是纯 Dart 实现理论上跨平台没有问题但鸿蒙的 Flutter 对dart:io的支持并不完全一致特别是 socket 的一些原生行为。ssh_key这个包在生成密钥时会用到系统随机数Android 上直接走Random.secure()鸿蒙上底层能力不同偶尔会出现密钥生成速度极慢的情况。我的替代方案是用dart:io的Socket.connect做一个底层 TCP 探测通道不经过http这样能拿到更纯粹的连接耗时。把密钥存储从shared_preferences和本地文件改为通过 Flutter 插件调用鸿蒙的ohos.security.asset避免密钥裸存在沙箱文件里被清掉。SSH 握手的校验逻辑保留在 Dart 层但连接层探测完全交给原生通道因为鸿蒙网络栈对connectTimeout的处理和 Android 不一致。这条策略的核心是“能纯 Dart 解决的不动必须动平台能力的就隔离成接口”。如果一开始不把策略定下来后面改着改着就会变成“为了适配而适配”状态机的可测试性会被严重破坏。2.3 适配原则不碰Dart层只补平台缺口我自己定下的适配原则只有三条第一Dart 层的现有 API 命名和同步模式一概不换保证老业务逻辑零迁移第二凡是可能被系统拦截的底层调用全部收口到AtServerStatusPlatform抽象类里用 FederatedPlugin 的形态放到ohos目录第三原生层只做测量和上报不做任何状态决策。为什么这样做at_server_status本身在业务侧已经被很多项目用了如果我把 API 从AtServerStatus改成AtServerStatusOhos会导致调用方全部重写。保持 API 不动同时允许动态注册平台实现才是最稳妥的插件化方案。在 Dart 侧只需要这样一段注册逻辑class AtServerStatus { static void _registerPlatform() { if (Platform.isAndroid || Platform.isIOS) { // 原有实现 } else if (Platform.isOhos) { AtServerStatusNative.instance OhosAtServerStatusNative(); } } }这块看起很朴素但直接避免了“if (Platform.isOhos) else”满代码飞的情况。后面新增 Windows 或者 macOS 支持也只需要再补一个平台实现类。3. 核心改造过程实录3.1 创建 ohos 插件壳我选择用 Flutter 插件模板来管理原生代码这样at_server_status既可以作为本地路径依赖也可以后续发布到鸿蒙仓库。先建一个专门放平台适配代码的插件包结构大致如下at_server_status_ohos/ ├── ohos/ │ ├── build-profile.json5 │ ├── oh-package.json5 │ └── src/main/ │ ├── ets/ │ │ ├── AtServerStatusPlugin.ets │ │ └── AtServerStatusNative.ets │ └── module.json5 ├── pubspec.yaml └── lib/ └── at_server_status_ohos.dartohos插件本质上是标准 OpenHarmony 模块oh-package.json5里要声明 Flutter 插件的依赖{ name: at_server_status_ohos, version: 1.0.0, main: Index.ets, dependencies: { ohos/flutter_ohos: file:./flutter } }如果你把它集成到宿主工程这个flutter依赖路径要跟你的 Flutter 引擎模块对齐不然会报找不到Plugin的编译错误。这也是最容易被忽略的一环很多人折腾半天发现是依赖路径写错。3.2 把 at_server_status 打进鸿蒙工程宿主 Flutter 工程里需要同时引用at_server_status和at_server_status_ohos两个包。在pubspec.yaml里这样写dependencies: at_server_status: path: ../packages/at_server_status at_server_status_ohos: path: ../plugins/at_server_status_ohos然后执行flutter pub get。这里有个坑由于flutter_flutter是社区分支pub 命令对 ohos 平台识别有时不完整如果你在pubspec.yaml的flutter: plugin: platforms:里没有声明ohos原生插件就不会被自动注册。所以插件的pubspec.yaml里要显式声明flutter: plugin: platforms: ohos: pluginClass: AtServerStatusPlugin dartPluginClass: AtServerStatusOhosPlugindartPluginClass是鸿蒙适配特别有用的一点允许你在 Dart 侧写一个统一入口原生pluginClass只是负责注册 MethodChannel 和 EventChannel。3.3 网络探测、密钥存储、鉴权监控的鸿蒙实现网络探测这块我在原生层通过Socket做 TCP 连接并且把时间点记录到微秒级。核心 intention 是不把探测逻辑复杂化只返回四个字段connectSpentMillis、reachable、localAddress、errorDetail。下面是鸿蒙侧用 ArkTS 写的 TCP 探测核心方法省略了错误分支但保留了关键路径import { socket } from kit.NetworkKit; import { connection } from kit.NetworkKit; async probeTCP(host: string, port: number): PromiseRecordstring, Object { const start performance.now(); let result: Recordstring, Object { reachable: false, connectSpentMillis: 0, errorDetail: , }; const conn connection.getDefaultSync(); const netHandle await conn.getDefaultNet(); const tcpSocket: socket.TCPSocket await socket.constructTCPSocketInstance(); try { await tcpSocket.connect({ address: { address: host, port }, timeout: 5000 }); result.reachable true; result.connectSpentMillis Math.floor(performance.now() - start); } catch (err) { result.errorDetail JSON.stringify(err); } finally { tcpSocket.close(); } return result; }注意鸿蒙上socket.connect的timeout字段单位是毫秒但如果你传了超时参数还额外在 Dart 侧用自带 timeout 包一层两层超时会有优先级不一致的隐患。我的建议是原生层只设一个较大的兜底超时精确的业务超时控制交给 Dart 状态机不然很难排查“到底是鸿蒙超时了还是 Dart 超时了”。密钥存储则用鸿蒙的asset接口。原版库为了兼顾多平台把 ssh key 写到应用沙箱目录在 Android 上没问题但在鸿蒙 NEXT 上应用沙箱规则更严某些路径你写进去容易读出来没问题可一旦应用被系统清理密钥就没了。所以我改成走 Asset Storeimport { asset } from kit.AssetStoreKit; async function storeKey(alias: string, keyData: Uint8Array): Promisevoid { const query { label: alias, data: keyData, accessControl: asset.AccessControl.NORMAL_ACCESS, }; await asset.add(query); }密钥轮换场景还要先remove再add不能直接覆盖。这点跟 Android 的KeyStore行为有差异稍不留神就会出现“旧密钥还在但新密钥写不进去”的情况。鉴权监控跟网络探测稍有不同它更偏向业务层需要拿 atServer 的pkam握手响应和时间戳。这部分我保留在 Dart 层做因为要用到protocol的签名逻辑原生层不参与签名。原生层只把 TCP 探测结果、本地时间戳和密钥读取结果传给 Dart由 Dart 完成状态聚合。3.4 实时状态更新的 EventChannel 设计“实时”这个要求通过拉模式是做不到的。原版库主要靠轮询轮询的时间间隔通常在 2 到 5 秒。在鸿蒙上我增加了两种推送通道一种是鸿蒙网络状态变化的广播比如网络从 Wi-Fi 切换到蜂窝网络时主动触发一次探测另一种是本地 socket 断开事件由原生层监听后立刻上报。使用 EventChannel 把原生事件流引到 Dart 侧EventChannel _statusEventChannel const EventChannel(at_server_status/status_events); StreamAtServerStatusSnapshot get statusStream { return _statusEventChannel.receiveBroadcastStream().map((event) { return AtServerStatusSnapshot.fromJson(MapString, dynamic.from(event as Map)); }).handleError((e) { return AtServerStatusSnapshot.unknown(); }); }原生 ArkTS 侧用EventSink持续推送let eventSink: EventSink | null null; const channel new EventChannel(at_server_status/status_events, (req) { eventSink req.eventSink; return true; });推送逻辑上有一点要注意鸿蒙的 EventChannel 在应用进入后台后消息发送频率会被系统节流。如果你把后台状态息屏也算作离线就会产生误报。我最后的处理是在 Dart 侧对 AppLifecycleState 做一层过滤只有前台才记录状态变化事件后台只保留最后一个快照。这样做的原因是鸿蒙的省电策略会在后台收紧网络连接socket 断开事件在后台并不代表 atServer 真的离线而更可能只是本地进程被冻结。如果不加过滤很多用户会看到“状态在后台疯狂跳变”的糟糕体验。4. 运行效果与性能实测4.1 测试环境与监测指标我这边测试设备是华为 Mate 60 Pro 和一台 Dayu 200 开发板鸿蒙 API 12Flutter 分支基于 3.22DevEco Studio 5.0.0。监测指标分四块首次握手耗时、TCP 探测成功率、状态机聚合耗时、内存增量。首屏场景是 App 启动后自动拉取 atServer 状态。这里比较关键的是“透明”体验不能让用户等到状态结果出来才看到界面所以 UI 层先展示缓存态状态流到达后无缝刷新。实测下来冷启动到第一个状态快照输出约 680ms其中 Tokens 加载占了 300ms 左右TCP 探测占了 200ms剩下 180ms 是状态聚合和事件分发。TCP 探测成功率在正常 Wi-Fi 环境下是 100%在弱网环境信号强度 -95dBm会下降到 84%但这 84% 不是误判而是真正的 TCP 连接超时。关键在于错误信息能准确区分“DNS 解析失败”“TCP 超时”“TLS 握手失败”这三类错误在errorDetail字段里会被明确标记Dart 侧就可以针对不同错误做不同的重试策略。4.2 透明度和实时性的权衡策略透明意味着用户能感知状态变化但频繁的通知会变成噪音。我在鸿蒙上采用了一个很简单的策略却意外地有效连续两次状态变化间隔低于 1.5 秒时不推送事件只更新内部快照只有状态“稳定变化”才推送。什么叫“稳定变化”比如从connected变到reconnecting如果 1.5 秒后又变回connected那么只保留最终状态不推送中间态。这样既保证了用户可以感知异常又不会被抖动搞得心慌。实时性这块EventChannel 的事件延迟在真机上平均 30ms 到 80ms基本可以忽略。但要注意的是事件推送的频率不能超过原生层 500ms 一次因为鸿蒙的 IPC 调用也有开销。我在原生层加了简单的节流阀同一状态下重复事件间隔小于 500ms 的直接丢弃。4.3 内存、耗电和延迟数据跑了一个小时持续监控App 的内存增量大约 12MB这个增量主要来自 EventChannel 的临时缓存和 Dart 对象快照不属于泄漏。GC 之后基本回到基线。耗电方面鸿蒙后台任务会把网络探测频率压得很低。我的实测结果前台 3 秒探测一次一小时增加耗电 5% 左右后台 30 秒探测一次几乎可以忽略。如果一直保持前台高频探测耗电肯定是硬伤。最后的方案是前台用 3 秒一个周期后台退到 15 秒并在系统广播网络变化时主动唤醒。延迟数据做个表格方便后面优化时对照阶段最小耗时平均耗时最大耗时说明TCP 连接18ms45ms280ms弱网下明显升高协议握手8ms24ms130ms受服务器负载影响状态聚合1ms3ms12msDart 层纯计算原生到 Dart 事件分发15ms38ms110msIPC 耗时变化这组数据也说明鸿蒙真实网络栈的 TCP 连接耗时并不比 Android 差太多但握手阶段因为涉及 atServer 密钥交换大头还是在网络 RTT 上优化的重心应该放在避免无效重试上。5. 常见问题与排查技巧实录5.1 编译期出现的 TypeError 和找不到模块鸿蒙 Flutter 插件最常见的编译错误是Cannot find module ohos/hypium或者Cannot find name EventChannel。前者是因为缺少测试依赖在oh-package.json5的devDependencies里补上 hypium 就行。后者通常是因为 Flutter 引擎模块没有正确导入需要在module.json5的dependencies里加入ohos/flutter_ohos。另外还有一个很隐蔽的错误ArkTS 的严格模式不允许使用Object作为无类型 JSON 的 catch 参数。很多从 TypeScript 转过来的开发者会写catch (e)这在 ArkTS 里会抛 “catch parameter must be typed” 的编译错误。我全部改成了} catch (err) { const typedErr err as BusinessError; result.errorDetail ${typedErr.code}: ${typedErr.message}; }5.2 运行时鉴权失败与密钥问题适配后第一次跑真实 atServer返回的鉴权结果是pkamVerification: false。排查下来不是库的逻辑问题而是鸿蒙上沙箱路径变了。原版库在 Android 上习惯把 atKeys 文件放在getApplicationSupportDirectory()鸿蒙当前版本对这个目录的写权限有调整写入后进程重启可能丢失导致 SSH 私钥读出来是空字符串。解决办法是把密钥读取路径统一收敛到原生 Asset Store然后通过 MethodChannel 提供给 Dart。注意要保留多份密钥备份因为服务器密钥轮换需要同时使用新旧密钥傻乎乎只存一份会导致轮换失败。这算是我这次适配中最值得写出来的经验。5.3 鸿蒙权限配置清单如果你的应用要做 TCP 探测必须在module.json5里声明ohos.permission.INTERNET。这个权限没什么坑但真正容易漏的是网络状态监听权限ohos.permission.GET_NETWORK_INFO以及后台访问网络的特殊权限。没有GET_NETWORK_INFOconnection.getDefaultNet()可能会返回一个空句柄排查问题时非常误导。鸿蒙的权限配置位置有两种src/main/module.json5里的requestPermissions字段以及在acls里配置受限权限。一般网络探测只涉及普通权限不需要特殊 ACL。如果你在开发板上调试还需要注意设备是否已经开启“允许后台应用联网”的设置这个开关在设置里的位置比较深实际测试时常被忽略。5.4 配套的排查速查表症状可能原因处理方式找不到 ohos 平台Flutter 命令是官方版切换到 flutter_flutter 社区分支EventChannel 收不到事件插件未注册或 EventSink 保存失败检查 oh-package.json5 依赖TCP 探测全部超时缺少 INTERNET 权限加入 module.json5状态在后台疯狂跳变鸿蒙后台网络策略限制Dart 侧过滤后台生命周期事件pkamVerification false密钥存储路径丢失改用 Asset Store 持久化编译时报 BusinessErrorcatch 参数未指定类型使用(err as BusinessError)这张表是我在适配过程中真实遇到的跟常见博客里贴的“标准答案”不一样都是拿鸿蒙真机一个个试出来的。6. 可以直接抄的集成代码片段6.1 Dart 侧调用示例如果你想在业务侧快速接入at_server_status的鸿蒙适配版本可以参考下面这个简化调用。核心逻辑是订阅状态流同时保留手动刷新入口。import package:at_server_status/at_server_status.dart; import package:flutter/services.dart; class AtStatusController { StreamSubscriptionAtServerStatusSnapshot? _sub; void start() { // 先注册一个占位实现避免拿到空的 AbstractError AtServerStatusNative.instance ?? OhosAtServerStatusNative(); final statusService AtServerStatus()..startMonitoring(); _sub statusService.statusStream.listen((snapshot) { if (snapshot.connectionState ConnectionState.connected) { print(${snapshot.atSign} - online, latency ${snapshot.latencyMillis}ms); } else { print(${snapshot.atSign} - offline, cause: ${snapshot.reason}); } }); statusService.refresh(); } void dispose() { _sub?.cancel(); } }这段代码里没有写任何平台特定的分支因为平台适配细节已经被抽象掉了。这也是我坚持“不碰 Dart 层 API”的回报业务侧不需要关心你到底用的是鸿蒙还是 Android。6.2 ohos 侧配置清单在宿主鸿蒙工程里需要确保oh-package.json5中存在 Flutter 引擎依赖同时module.json5设置好权限。我提取了一份最小清单{ module: { name: entry, type: entry, requestPermissions: [ { name: ohos.permission.INTERNET }, { name: ohos.permission.GET_NETWORK_INFO } ], dependencies: [ { name: flutter_ohos, version: 4.0.0 } ] } }如果实际开发中发现GET_NETWORK_INFO不被识别检查你的鸿蒙 SDK 版本是否太老。API 11 之前还没有这个权限常量需要用兼容写法直接字符串声明。这种版本兼容问题在社区分支里尤其常见因为 Flutter SDK 和鸿蒙 SDK 版本之间不是一一对应的。6.3 自动化回归脚本最后给一个小建议适配完平台层一定要把自动化回归跑起来。鸿蒙的 Flutter 集成测试用flutter test integration_test/ -d device但前提是integration_test插件也适配了 ohos。我这边直接用 hdc 驱动写了一个非常简单的 shell 脚本做冒烟测试#!/bin/bash hdc shell aa start -a AbilityName -b com.example.astatus sleep 3 hdc shell cat /data/app/el2/100/log/at_status_smoke.log这个脚本不依赖测试框架只验证启动后日志里是否出现online或offline两种正常状态。如果出现platformException说明平台通道没通需要回到前面第 5 节的表格排查。把冒烟脚本挂在 CI 上比手动点点点靠谱得多。个人经验说一句这种插件适配最怕的不是语法不会而是你不知道哪一层出了问题。所以日志一定要打全errorDetail必须包含错误码、错误信息、调用栈否则真机上报 bug 时你根本无从判断是鸿蒙原生层抛错还是 Dart 状态机跑飞了。把日志分类打好这个项目的一半工作量就算完成了。
网站建设高端定制企业官网