当前位置:首页>鸿蒙APP>我竟然发现了原生鸿蒙的一个底层 bug

我竟然发现了原生鸿蒙的一个底层 bug

  • 2026-10-11 18:06:49
我竟然发现了原生鸿蒙的一个底层 bug

我准备连载一个原生鸿蒙开发系列,希望通过系列文章带你一同了解我是如何从0开发一款原生鸿蒙软件的,欢迎订阅关注。

一次持续 12 小时的原生鸿蒙开发调试经历

★ 

我花了一周时间调试一个 HarmonyOS 网络请求问题。模拟器上 2010 条备忘录完整拉取,真机上同一份代码只拿到 175 条,而且全是 PUBLIC——没有一条 PRIVATE。我查了 HTTP 参数、协议版本、gzip 解压、缓冲区大小,甚至写了一篇报告断言问题是 gzip。但最终真相是:@kit.NetworkKit在真机上的 HTTP 请求特征,被服务端识别为"低权限客户端",数据被主动过滤了。

”

一、背景

我在做一个鸿蒙原生 memos 客户端。后端是 memos v0.29.1 + OpenResty,客户端用 @kit.NetworkKit(@ohos.net.http)发送 HTTP 请求。开发时一切顺利——Pura90 模拟器上 2010 条备忘录(含 PRIVATE)完美同步。

然后上真机。

同一份 HAP 包、同一网络、同一份 PAT 鉴权 Token,真机只拿到 175 条。全是 PUBLIC,PRIVATE 一条没有。

平台
备忘录数量
是否包含 PRIVATE
Android(OkHttp)
2010 条
✅ 全部
HarmonyOS NEXT 模拟器
2010 条
✅ 全部
HarmonyOS NEXT 真机 Pura90仅 175 条
❌ 全是 PUBLIC

模拟器正常、真机异常。这个问题本身就是一个反常识的信号。但我当时没在意,以为只是某个参数没对齐。

二、第一轮排查:那些"看似合理"的假设

2.1 HTTP 参数对齐

假设:鸿蒙 app 的 API 调用参数与 Android 版不一致。

差异点
Android(OkHttp)
HarmonyOS(原始)
filter
参数
filter=creator=="users/gujin03"
未传
pageToken
始终传 pageToken=&
首次请求不传
参数顺序
pageSize→pageToken→state→filter
顺序不同

全部对齐后,数据量仍 175。❌

2.2 HTTP 协议版本

假设:HTTP/2 在真机上有兼容问题。

真机 HTTP/2 报 2300999 Internal error,改成 HTTP1_1后稳定了,但数据量还是 175。模拟器依然 2010。

结论:问题不在协议版本,在真机 vs 模拟器的环境差异。

2.3 Gzip 解压假设(最有迷惑性的方向)

这是一个让我浪费了大量时间的假设。服务端响应体通过 Transfer-Encoding: chunked传输,我在模拟器上观察 response.result大小约 37KB,真机上只有约 24KB——差了约 40%。

运行环境
response len
同步数据量
模拟器
~37KB
2010 条 ✅
真机
~24KB
175 条 ❌
curl(服务端直接测)
~41KB
2010 条 ✅

我写了一整份问题报告,断言是 @kit.NetworkKit没有自动解压 gzip,导致二进制压缩数据被当作 UTF-8 字符串解码后截断。

但我不放心,决定用 HttpShark(底层 libcurl)在真机上直接发同样的请求做个对比。

三、决定性证据:同一台真机上的对照实验

HttpShark 在同一台 Pura90 真机上、同网络、同 PAT,发送完全相同的 HTTP 请求:

HTTP 客户端
运行位置
返回数据量
有 nextPageToken?
curl(电脑端)
云端服务器
30 条
✅
HttpShark/libcurlPura90 真机30 条
✅
@kit.NetworkKit
Pura90 真机
25 条
❌ 丢失

这个结果直接锁定了问题范围:

  1. ✅ 服务端正常——对真机 IP 返回的数据与电脑端一致
  2. ✅ 真机网络正常——libcurl 在真机上能完整收发
  3. ❌ 问题在 @kit.NetworkKit本身

我当时写下了结论:"@kit.NetworkKit的 gzip 解压有 bug"。这是对的,但不完整。

四、第二次反转:不是 gzip

我准备提交 bug report 之前,决定在 VPS 上直接 curl 内部端口确认一下 gzip 的情况:

ounter(linecurl -sI http://127.0.0.1:5230/api/v1/memo?pageSize=30

结果:

ounter(lineounter(lineContent-Type: application/jsonContent-Length: 37541

没有 Content-Encoding: gzip。响应体以 {"memos":明文 JSON 开头。

我追查了两天的 gzip 假设,是错的。

五、根因发现:那 175 条数据全是 PUBLIC

真正的转折点来自一个被我反复忽略的细节——175 条数据全部是 PUBLIC。

我回想了服务端 memos 的行为:memos API 会根据客户端权限返回不同 visibility 的数据。如果是匿名或低权限请求,只返回 PUBLIC。

在 VPS 上用 curl 加同样 PAT 验证:

ounter(linecurl -s "https://memos.mysite.xyz/api/v1/memos?pageSize=200"

返回 200 条中,PRIVATE 有 191 条,PUBLIC 仅 9 条。PRIVATE 和 PUBLIC 散布在时间线上。

关键推理:如果只是数据截断(如 gzip 未解压),app 应该同时看到 PUBLIC 和 PRIVATE——因为它们在 JSON 中穿插排列。但 app 只看到 PUBLIC,说明服务端在返回数据之前就已经做了 visibility 过滤。

根因链至此浮现:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineapp 使用 @kit.NetworkKit 发送 HTTP 请求→ 真机上的 @kit.NetworkKit 实现与模拟器不同→ 发出的 TLS 指纹 / HTTP 请求特征独特→ OpenResty/memos 将其识别为"低权限客户端"→ 服务端主动过滤,只返回 PUBLIC 数据→ 175 条(全是 PUBLIC)

六、深度分析:@kit.NetworkKit的什么行为触发了过滤?

6.1 不是 gzip,是人

gzip 假设之所以有迷惑性,是因为数据量确实少了(24KB vs 37KB)。但这个数据差异是 visibility 过滤的结果——过滤后 JSON 体自然变小了,不是因为解压失败。

6.2 模拟器 vs 真机的差异根源

因素
模拟器
真机
TLS 指纹
模拟系统 OK
真实设备指纹
,被 OpenResty 识别
HTTP 库特征
不被识别
被 memos 检测为"受限类型"
@kit.NetworkKit
实现
完整版
存在实现差异

同一个 API 调用,模拟器上 @kit.NetworkKit请求的 TLS ClientHello 和 HTTP 头顺序,与真机不同。OpenResty 通过 lua-resty-limit-traffic或自定义检测规则,将真机的请求归类为"非标准客户端"。

6.3 为什么 HttpShark(libcurl)正常?

HttpShark 使用 libcurl 作为底层 HTTP 引擎,与鸿蒙 @kit.NetworkKit的网络栈实现完全独立:

  • 不同的 TLS 客户端问候(ClientHello)
  • 不同的 HTTP 头顺序
  • 不同的 ALPN 协商方式

libcurl 的请求特征被 OpenResty 识别为标准桌面/服务端客户端,因此获得了完整权限。

@kit.NetworkKit在真机上的 HTTP 请求特征与标准浏览器/库不同,被服务端误判为低权限客户端——这就是原生鸿蒙的一个底层 bug。

七、解决方案与启示

7.1 终极方案:NAPI libcurl HTTP 模块

既然 @kit.NetworkKit的行为不可控,那就换掉它。通过 NAPI 将 libcurl 编译为原生 .so 模块,在 ArkTS 中调用:

ounter(lineounter(lineounter(lineounter(lineounter(lineArkTS HttpUtil.ts    → curlNet.curlRequest({url, method, caBundle})        → C++ napi 桥接            → curl_easy_perform() 在 worker 线程执行                → 返回 Promise<T> 到 ArkTS
方案
可行性
效果
A. NAPI libcurl HTTP(采用)
✅
✅✅ 彻底解决
B. NAPI zlib 手动解压
❌
根因不是 gzip,打了空靶子
C. 等待鸿蒙修复
⏳
时间不确定

最终验证结果:

指标
修复前
修复后
请求次数
~134 次(pageSize=15)
5 次(pageSize=400)
同步耗时
被杀进程(THREAD_BLOCK_6S)
~16 秒
获取数据
175 条 PUBLIC
2004 条
PRIVATE 备忘录
❌ 全部缺失
✅ 全部正常

7.2 经验与教训

  1. 模拟器正常不代表真机正常。@kit.NetworkKit在模拟器和真机上的底层实现不同,这个差异足以导致完全不同的服务端行为。

  2. "数据少了"不等于 gzip。我花了两天追一个 gzip 假象,原因是我只看了"数据量少了"这个表象,却没有验证响应头的 Content-Encoding。

  3. 异常数据分布比数据总量更有价值。175 条全是 PUBLIC 这个信号被我忽略了整整两天。如果我在第一天就注意到这个模式,排查时间能缩短 80%。

  4. 抓包工具的对照实验是最有效的诊断手段。HttpShark(libcurl)在同一台真机上正常工作,直接锁定了问题范围。没有这个实验,我可能还在调 CURLOPT 参数。

  5. 错误信息要暴露原始内容。登录页 catch 块用固定文字掩盖 (e as Error).message,让 SSL 证书问题的排查多花了两天。

7.3 实践建议

场景
推荐做法
原因
HarmonyOS 真机 HTTP 请求
NAPI libcurl 替代 @kit.NetworkKit
绕过底层实现差异
涉及权限/visibility 的 API
检查数据分布模式
总量差异可���误导根因方向
真机 vs 模拟器表现不一致
立即上抓包工具
锁定问题范围比调参数更重要
catch 块错误处理
暴露原始错误信息
固定文字会掩盖根本原因

最终,@kit.NetworkKit的 bug 并没有被修复——我换了一条路走。但这件事让我明白一个道理:在真机上看到的现象,如果在模拟器上复现不了,那问题大概率不在你的代码里,而在底层。原生鸿蒙还很年轻,有些 bug 是真实存在的。能做的就是发现它们、记录它们、然后找到自己的路绕过去。

如果你觉得今天这篇有收获,欢迎点赞、在看、转发三连,我们下篇见

赞
在看
收藏

THANKS FOR READING

最新文章

随机文章