
1. 为什么是 mercury_client鸿蒙网络请求的核心痛点做 Flutter 开发的人都知道插件生态的适配速度永远追不上系统版本的更新速度。前阵子团队接到一个需求把一套基于 Flutter 的跨端应用搬到鸿蒙系统上别的功能还好说唯独网络层炸了锅——原本用的 dio 和 http 包在鸿蒙上各种姿势翻车要么请求发不出去要么缓存失效导致页面反复loading。折腾到后面我们直接放弃通用方案开始逐个适配底层网络引擎这才把目光锁在了 mercury_client 这款冷门但硬核的三方库上。mercury_client 不是一个简单的 HTTP 封装它在 dart:io 的 HttpClient 之上做了一层非常讲究的抽象自带高性能内存缓存、支持连接复用、内置超时重试机制还能在请求级别做优先级调度。对于鸿蒙这种初期 Flutter 社区支持还不算成熟的平台来说它底层的可替换性反而成了救命稻草——你不能直接跑 dart:io但你可以把它的传输层换成鸿蒙原生的网络栈上层的缓存、队列、拦截器逻辑完全不用动。适配这件事听起来玄乎但本质无非三件事让请求发得出去、让缓存靠得住、让连接稳得住。鸿蒙的 Flutter 引擎基于 OpenHarmony 的自研渲染与桥接层很多原本依赖 C 实现的 socket 行为并不能无缝映射到 posix 接口上所以你必须理解 flutter 插件如何在鸿蒙上注册通道、如何调用 ohos.net.http 的能力、如何把 dart 侧的 Future 与原生侧的异步回调对齐。这篇文章我会把这套适配流程完整拆给你看包括我踩过的坑、反复验证过的缓存参数、以及最终压测的数据表现。无论你是刚接触 Flutter 鸿蒙适配还是已经在维护自研网络层这篇指南都能让你少走至少两周的弯路。1.1 鸿蒙上的 HTTP 请求现状先给没上过鸿蒙的同学说下现状。鸿蒙的 Flutter 支持目前在持续完善中但 Dart 层能直接用 dart:io 吗能但很不稳定。原因是鸿蒙的 socket 实现与 Linux 内核 API 并不完全一致部分网络接口在低版本鸿蒙设备上会有 socket 连接超时、DNS 解析失败甚至直接 crash 的问题。我实测过用标准 HttpClient 在 HarmonyOS NEXT 开发者预览版上连续请求一个 HTTPS 接口大概二十个请求里就会出现两三个 SocketException这在生产环境是完全没法接受的。所以行业里通用的做法是走 Platform Channel把 HTTP 的活儿交给鸿蒙原生层去干。鸿蒙原生提供了 ohos.net.http 模块支持标准的 HTTP/1.1、HTTPS、连接池、gzip 解压、证书校验等能力。你需要做的就是在 Flutter 侧写一个抽象接口底层分别实现 dart:io 和 ohos.net.http 两套实现然后通过 factory 模式切换。mercury_client 的好处在哪它本身就把 HttpTransport 抽象成了独立接口里面定义了 openUrl、close、get、post 等方法签名你只需要为鸿蒙写一个 OhosHttpTransport 并把它注入进去就行。这是我认为这套方案最终能走通的最关键前提。1.2 mercury_client 的设计亮点缓存、连接复用、稳定性很多同学会问既然都要用原生网络栈了那我直接用 MethodChannel 请求再自己写个 Map 缓存不行吗行但你要面对的不只是“能发请求”还有缓存穿透、缓存雪崩、连接建立成本、线程调度等一系列细节。mercury_client 把这些东西都打包好了而且它的内存缓存设计得相当有意思。它不是简单的 MapString, Response而是基于 weight 的 LRU 缓存允许你配置最大权重、过期时间、以及按请求路径做条件缓存ETag/Last-Modified。另一个亮点是连接复用。HTTP 层的连接复用对性能影响极大尤其在弱网环境下。TLS 握手 TCP 三次握手一次新连接大概要多花 200ms 到 1s 不等。mercury_client 内部维护了一个连接池对相同 host 的请求复用底层 socket再加上 HTTP keep-alive可以让你的中长列表页在连续滚动时请求延迟下降 30% 以上。这些能力在鸿蒙原生网络栈里其实也有但要让 Flutter 侧能精确地控制超时、重试和优先级就得靠引擎层做一层聪明映射。我们这一版适配就是把 mercury_client 的传输层完整替换成 ohos.net.http再把它的缓存和调度层原封不动地保留了下来。2. 鸿蒙化适配的前期准备与环境搭建这部分我会假设你已经有一个能跑的 Flutter 工程并且熟悉 Flutter 的基础命令。如果你是从零开始可能需要先花半天时间把 Flutter SDK、鸿蒙 SDK 以及 DevEco Studio 的环境对齐。鸿蒙的 Flutter 适配目前有官方维护的 flutter_flutter 分支社区常称为 “Flutter for OpenHarmony”装好之后你用 flutter doctor 就能看到 ohos 工具的检查项。准备工作的实质是让 Flutter 引擎知道你的目标是 ohos而不仅仅是 android 或 ios。这套链路里最容易出问题的是版本匹配Flutter SDK 和鸿蒙 SDK 版本必须严格对应否则编译出的产物会出现接口找不到、符号无法解析等问题。我的建议是使用官方提供的一体化工具链不要手动混搭 Flutter 3.7 与鸿蒙 API 9 这种组合除非你想体验半小时一崩溃的编译体验。2.1 安装 Flutter 与鸿蒙 SDK版本对齐是命门直接说版本号。我当前用的是 Flutter 3.7.12 对应的 ohos 分支搭配 HarmonyOS SDK 的 API 9 和 DevEco Studio 4.0。这套组合在社区里验证过的人最多坑相对最少。如果你用更新的 Flutter 3.10 或 3.13可能接口有变化但大逻辑一致。安装步骤没什么花头但有几个细节必须注意Flutter 的 bin 目录要加入 PATH并且不能与 Android 的 flutter 冲突鸿蒙 SDK 的路径要写在 local.properties 里字段名一般是 harmony_sdk_dirDevEco Studio 打开 Flutter 工程时需要选择“Open HarmonyOS Project”而不是普通 Flutter 工程。我自己第一次装的时候就是忽略了 local.properties 里的路径配置导致一直报“ohos toolchain not found”的错误。这种问题常见但很蠢提前检查好环境变量能省掉很多摸索时间。2.2 建立鸿蒙工程与 Flutter 模块的连接这里说一下工程结构。鸿蒙 Flutter 应用通常是一个 DevEco 工程里嵌套了一个 Flutter moduleFlutter 代码通过 so 包和 assets 的形式打到鸿蒙的 hap 包里。你要做的第一件事是在工程目录下执行flutter build hap --debug这个命令会生成 Flutter 的产物以及一个用于桥接的 ohos 插件壳工程。openharmony 的 flutter 适配有一个核心机制叫 “Flutter 的 platform channel 映射到 ohos 的 ability/page 生命周期”也就是说Dart 侧往原生发消息时会通过 SystemChannel 走到 ohos 的 Flutter 容器里再由容器转发给你注册的插件。mercury_client 的鸿蒙化需要用到这样的链路Dart 侧封装统一的请求接口底层通过 channel 与 ohos 原生通信原生拿到请求参数后调用 ohos.net.http 完成实际请求再把结果序列化回 Dart。这块的注册代码比较简单但务必注意异步回调的线程切换原生侧的网络回调通常在非 UI 线程而 Flutter 侧的消息通道要求线程安全建议使用 ohos 的 taskpool 或者 event runner 把结果切到主线程再返回。2.3 依赖管理与版本坑改依赖之前先看一眼你的 pubspec.yaml。建议直接使用 git 依赖而不是 pub.dev 上的旧版本因为 mercury_client 的鸿蒙适配分支可能还没有正式发布到仓库。你可以这样声明dependencies: mercury_client: git: url: https://github.com/your-fork/mercury_client.git ref: ohos-support这样有个坏处如果上游更新了你需要手动 merge。我通常会把 fork 的仓库固定 commit这样 CI 构建时可复现。还有一点是小心 dart:io 和 dart:isolate 在鸿蒙上的兼容性问题。mercury_client 内部有些文件直接 import 了 dart:io比如 File 缓存、Socket 相关操作。如果原生网络栈走的是 ohos你需要把这些文件用条件导入替换掉。条件导入是 Dart 自带的能力import transport_io.dart if (dart.library.ohos) transport_ohos.dart;这里dart.library.ohos是鸿蒙 Flutter 环境内置的 library 标识可以准确区分当前运行平台。替换之后注意代码里不能有未引用的遗留 import否则编译会报 unresolved 错误。3. mercury_client 鸿蒙化改造的关键步骤这一章是实操核心我会按自底向上的顺序来讲。先搞定传输层再搞缓存和调度最后是整体对接。每一步都写清楚为什么这么做参数怎么定你要改哪些文件。按我的经验这整套改造量大概在 700 行 Dart 代码加 300 行 Java/Kotlin 代码左右熟练的话两到三天能搞定。3.1 剥离 dart:io 依赖I/O 与网络层抽象mercury_client 的源码结构比较清晰核心在lib/src/目录下。你要找到transport.dart文件里面定义了一个抽象类HttpTransport接口大概长这样abstract class HttpTransport { FutureTransportResponse send(TransportRequest request); FutureTransportConnection connect(Uri host); void close(); }而真正实现HttpTransport的类在transport/io_transport.dart里它直接使用了 dart:io 的 HttpClient。鸿蒙化的时候你新建一个transport/ohos_transport.dart把 send 和 connect 都改为通过 MethodChannel 或 EventChannel 调用原生。这里有个细节dart:io 的 HttpClient 默认帮你处理了重定向、cookie、gzip 解码等能力。但你换成 ohos 原生之后这些能力需要你手动开启。ohos.net.http 的 HttpRequest 支持设置followRedirects、cookie、header你需要在原生侧把这几个参数都接收下来并逐一配置到 ohos.net.http.HttpRequest 中。我在改造时就把 gzip 处理的逻辑放在了 Dart 层if (response.headers[content-encoding] gzip) { final decompressed await _decodeGzip(response.bodyBytes); response response.replace(bodyBytes: decompressed); }放在 Dart 层的好处是能复用 mercury_client 原有的拦截器逻辑而且后续如果换别的原生栈不需要重复实现。坏处是如果响应体极大内存会承受压力但大部分接口响应在几 KB 到几百 KB问题不大。3.2 HTTP 通道的鸿蒙原生实现ohos.net.http 的正确姿势原生侧的核心是写一个 FlutterPlugin注册 MethodChannel然后在onMethodCall里处理sendRequest。先上代码框架public class OhosHttpPlugin implements FlutterPlugin { private static final String CHANNEL mercury_client/http; private MethodChannel channel; Override public void onAttachedToEngine(FlutterPluginBinding binding) { channel new MethodChannel(binding.getBinaryMessenger(), CHANNEL); channel.setMethodCallHandler(this); } Override public void onMethodCall(MethodCall call, Result result) { if (sendRequest.equals(call.method)) { HttpRequestOptions options call.arguments(); sendAsync(options, result); } else { result.notImplemented(); } } }在鸿蒙里你实际使用的能力是ohos.net.http.HttpClient它提供request(url, options)方法返回一个 Promise 或者回调。由于 Java/Kotlin 侧无法直接访问 JS 的 Promise一般用 ExpressionStatement 或者 import 一个ohos.net.http.HttpClient的单例。以下是通过StageModel的方式调用本质上跟 Android 的网络库类似但需要导入ohos.net.http包。请求回调回来之后你要把 headers、statusCode、body 字节流都封装成 Map再通过result.success(map)返回到 Dart 层。一个简单又不漏字段的模型可以这样val resultMap HashMapString, Any?() resultMap[statusCode] response.responseCode resultMap[headers] response.header resultMap[body] response.result // 字节数组或字符串 result.success(resultMap)注意鸿蒙HttpRequest的响应体默认是字符串如果你的接口是二进制流需要额外设置HttpRequestOption的expectDataType为DataType.ARRAY_BUFFER。否则图片、文件流都会变成乱码字符串。我一开始没注意图片一直加载失败排查半天发现是数据类型问题。3.3 内存缓存的实现要点LRU 与并发访问控制mercury_client 自称“自带高性能内存缓存”这个缓存的核心是CacheStore类内部维护了一个 LinkedHashMap 作为 LRU 容器配合读写锁做到并发安全。鸿蒙化的时候有一个问题dart:io 的 File 可以用来做内存缓存的 spill-over但在鸿蒙上 File 的 API 也是可用的只是它映射到底层文件系统的方式不同而且频繁写磁盘会导致 IO 竞争。我建议在鸿蒙版本上把缓存完全做成纯内存模式不做磁盘降级。原因很简单鸿蒙设备的文件 IO 在 flutter 侧目前性能还不太稳定尤其是一些低端机器磁盘速度偏低反而拖慢请求。如果你的应用对缓存容量要求特别大可以通过配置项maxWeight来控制设置多大合适按经验来说应用类型建议缓存容量说明资讯阅读类20-50 MB适合缓存列表、图片缩略图IM 社交类5-15 MB缓存消息内容避免图片频繁重复加载视频类100-200 MB纯内存建议慎用大流量场景建议只缓存封面图实际上在纯内存缓存里maxWeight通常控制键值条数或字节数。mercury_client 默认计算方式会遍历所有 entry 的字节数总和超过上限就移除最久未使用的。记得把maxEntries也设置上防止单个 key 的值很大时一次占满缓存。下面是我在适配时修改缓存驱逐逻辑的方法原来驱逐是按 insertion order但 LRU 应该按 access order。需要把LinkedHashMap的 accessOrder 参数设为 true这样才能在每次读取缓存时把它移动到链表尾部。这在 Dart 里也有类似的数据结构如果你不想手动实现可以直接用lru_cache包不过为了少一个依赖我还是自己改了 20 行代码。3.4 请求合并与连接复用对抗高并发糟糕的网络请求框架在高并发下会出现 thundering herd 问题多个相同请求同时发出每个都建立新连接后端压力翻倍。mercury_client 在 Dart 层做了请求合并Request Coalescing。当一个请求在途时相同 URL 的新请求会先挂起等第一个请求返回后直接把结果分发给所有等待者。鸿蒙原生侧同样可以配合做连接复用。ohos.net.http的 HttpClient 默认维护连接池所以你的 TTransport 实现里不要每次 sendRequest 都新建 HttpClient 实例而是定义成一个单例。这是我在原生侧代码里特别留意的点object HttpClientHolder { val client: ohos.net.http.HttpClient by lazy { ohos.net.http.HttpClient() } }如果你每次请求都 new 一个 client连接池就完全没用了TLS 握手的开销会全部打回原形。连接池的复用参数如最大连接数、keep-alive 时间在鸿蒙里面也有配置入口但大部分情况下默认值够用。如果发现高并发下连接数被打满可以尝试调大最大连接数或者设置空闲超时更久一点。另外你也需要在 Dart 层实现一个简单的 debouncer避免用户快速滑动时发送大量重复的图片请求。我的做法是在 mercury_client 的 interceptors 列表里插一个DebounceInterceptor规定 500ms 内相同 key 的请求只发一次后续的直接走缓存。4. 适配中的常见问题与排查技巧这章是我认为最有价值的部分因为官方文档不会写这些。每一个问题都是我实际踩过的或者社区里高频出现的。我整理成速查表然后挑几个典型的展开讲。4.1 证书与网络安全配置TLS 握手失败鸿蒙的网络安全策略比 Android 更严格。默认情况下如果你的服务器 HTTPS 证书链不完整或使用自签名证书ohos.net.http 会直接拒绝连接Dart 侧拿回的错误往往是SocketException: Connection failed或者HandshakeException。排查时建议先开 Charles 抓包确认 TLS 握手阶段的细节。在鸿蒙原生侧如果要信任自签名证书需要用到HttpRequestOption里的certificate参数或者配置网络配置文件network_security_config.json。这里重点提醒生产环境不要全局信任自签名证书除非你真的很清楚自己在做什么。调试时可以临时配置上线前一定要改回来。4.2 内存缓存溢出与抖动问题我们曾经在鸿蒙上遇到过一种很奇怪的现象列表页滑动时内存突然暴涨然后 OOM。后来定位到是缓存 图片解码双重问题。mercury_client 缓存的是原始字节如果列表里是一堆高清大图光原始字节就能撑爆内存。建议在缓存之前统一做一次压缩或者改用ImageProvider的缓存策略。如果你的应用主要是文本接口那 mercury_client 的字节缓存非常安全但要是图片居多建议把缓存权重调小并且把图片格式转成 WebP 再缓存能减小 50% 以上的体积。另外内存缓存一定记得处理并发读写。我在适配时把 Dart 的CacheStore内部的锁从synchronized改成了Semaphore因为它不支持 reentrant某些场景下会导致死锁。如果你使用了自己实现的锁建议打印一下线程堆栈。4.3 弱网下的超时与重试策略鸿蒙设备的弱网表现差异很大不同机型在相同网络环境下的 RTT 可能翻倍。mercury_client 允许你配置连接超时、读取超时、总超时但我发现鸿蒙原生的HttpRequest的connectTimeout与读取超时是分开的如果只配了 connectTimeout读取超时会退化为默认的 30 秒这时候用户等待时间过长体验很差。我的建议是connectTimeout: 10sreadTimeout: 15s总超时: 20s重试次数: 2 次幂等请求可设 3 次重试逻辑不要放在原生否则你很难控制重试的触发条件。在 Dart 侧实现一个RetryInterceptor只有遇到TimeoutException或者 5xx 状态码时才重试并且遵循指数退避0.5s、1s、2s。这样幂等 POST 也可以安全地重试。4.4 调试工具Charles 与鸿蒙抓包技巧排查网络问题抓包是必不可少的。鸿蒙原生应用可以走系统代理Charles 配置好 SSL 代理后就能看到 HTTPS 明文。但 Flutter 侧的 dart:io 请求默认可能不走系统代理所以如果你在 Flutter 侧抓不到包可以先把请求切到 ohos 原生通道这样 Charles 就能看到了。多说一句使用 Charles 抓包时记得安装并信任 Charles 的 CA 证书否则 HTTPS 流量无法解密。这个证书安全问题在鸿蒙上特别敏感团队里有同学在测试机上安装了 Charles 根证书之后忘了移除导致生产环境里部分接口出现信任错误折腾了一下午。用完测试证书一定要恢复原样。5. 性能对比与验证结果适配完了不能光说能跑得拿数据说话。我在两台设备上做了对比一台是运行 Android 12 的手机另一台是鸿蒙 4.0 的开发板两者同时跑同一个测试 App接口是 10 个并发请求每个请求返回约 15KB JSON 数据。5.1 冷启动与首包速度冷启动场景下也就是 App 刚打开、所有缓存为空时鸿蒙原生的 ohos 网络栈与 Android 的 OkHttp 基础性能接近。首包耗时差距在 5% 以内基本可以忽略。不过如果走 Flutter 的默认 dart:io首包耗时大约慢 10%-20%而且不稳定波动很大。换上 mercury_client 后稳定性明显更好连续十次冷启动的耗时标准差从 120ms 降到了 30ms 左右。对比数据如下场景dart:iomercury_client ohos冷启动首包平均耗时512 ms468 ms标准差118 ms31 ms10并发完成时间1.8 s1.2 s5.2 缓存命中率的影响接下来我测试了缓存对请求耗时的影响。用同一份列表数据连续请求 50 次前 2 次需要真正访问网络后 48 次理应从内存缓存获取。结果 mercury_client 的缓存命中率基本是 100%且缓存命中的请求耗时在 1ms 到 3ms 之间完全可以在 UI 线程同步完成。而如果走 dart:io 的 HttpClient即使你手动缓存了响应反序列化流程也会拖慢几毫秒列表滚动时能感觉到一点卡顿。5.3 压测稳定性压测是最能看出适配质量的。我写了一个脚本在 30 秒内连续发起 2000 个随机请求混合 GET 和 POST检测超时率、错误率和内存增长。适配前dart:io 方案在请求到第 1000 个左右时开始出现 SocketException错误率接近 0.5%内存波动超过 80MB。适配 mercury_client 之后错误率降到 0.02% 以下内存波动控制在 30MB 以内。而且由于走的是连接池设备帧率保持稳定没有出现暴卡的情况。当然这个结果也和我们设置的缓存策略有关。如果是纯随机 uuid 的请求每次缓存都 miss性能和内存表现会稍差但仍然能保持在可接受的范围。所以这里也提醒你缓存 key 的设计一定要合理能拆成 path query 组合就别用完整 URL 做 key否则缓存利用率很难上去。6. 一些适配时的额外心得最后聊点非技术层面的东西。跨平台适配这种事情最难的不是代码而是对整个链路有清醒的认识。你在鸿蒙上写一个网络请求它经过了 Dart、Flutter engine、Platform Channel、鸿蒙框架、系统 socket 五层任何一层出了小问题表象都是“请求失败”或“卡顿”。排查时如果只盯着 Flutter 侧很容易陷入死胡同。我习惯在适配初期就把日志分层逐级打印Dart 层打请求进入/返回Channel 层打消息发送/接收原生层打完后再打系统错误码。这样一次请求流程走完哪一层断了立刻就能看到。另外mercury_client 虽然相对冷门但它的模块化设计确实值得借鉴。很多网络库把传输层和业务层揉在一起导致你不能轻易替换底层实现。如果你未来也有适配鸿蒙、或者适配其他新系统的需求不妨在最初写网络层的时候就把 Transport 抽象出来留好扩展位。这小小的一步可能会在你大几周的时间。我在适配时保留了一支完整的 fork后面如果再遇到鸿蒙 API 变化直接改ohos_transport.dart就能快速同步。Flutter 和鸿蒙的适配生态还在快速迭代你手里这套方案大概率不会是一劳永逸的但只要抽象层次不坏升级成本就永远可控。