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 把这些东西都打包好了,而且它的内存缓存设计得相当有意思。它不是简单的 Map<String, 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_dir;
- DevEco Studio 打开 Flutter 工程时,需要选择“Open HarmonyOS Project”,而不是普通 Flutter 工程。
我自己第一次装的时候就是忽略了 local.properties 里的路径配置,导致一直报“ohos toolchain not found”的错误。这种问题常见但很蠢,提前检查好环境变量能省掉很多摸索时间。
2.2 建立鸿蒙工程与 Flutter 模块的连接
这里说一下工程结构。鸿蒙 Flutter 应用通常是一个 DevEco 工程里嵌套了一个 Flutter module,Flutter 代码通过 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 { Future<TransportResponse> send(TransportRequest request); Future<TransportConnection> 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 = HashMap<String, 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: 10s
- readTimeout: 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:io | mercury_client + ohos |
|---|---|---|
| 冷启动首包平均耗时 | 512 ms | 468 ms |
| 标准差 | 118 ms | 31 ms |
| 10并发完成时间 | 1.8 s | 1.2 s |
5.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 和鸿蒙的适配生态还在快速迭代,你手里这套方案大概率不会是一劳永逸的,但只要抽象层次不坏,升级成本就永远可控。