在 OpenHarmony 真机上跑 Flutter 应用,最让人头疼的不是 Dart 层语法报错,而是网络请求一旦出问题,你根本不知道请求到底发出去没有、参数长什么样、服务端到底回了什么。之前用 Android 时还能靠 Logcat 把整个报文扒出来,切到 OpenHarmony 后这套经验几乎作废,hilog 里全是原生侧日志,Flutter 的 print 输出要么找不到、要么被截断,而 Dio 默认在控制台打印的那一坨缩进混乱的内容,看多了眼睛真的会瞎。后来我在项目里逐步引入了 Pretty Dio Logger 作为统一的请求日志方案,并针对 OpenHarmony 的日志通道、权限模型和构建配置做了适配,才算把"网络请求监控"这件事真正打通。
这篇内容不适合只想看 API 怎么调用的人,也不适合完全没碰过 Dio 的纯新手。它会从 Flutter for OpenHarmony 的实际运行差异讲起,把 Pretty Dio Logger 的接入方式、hilog 日志捞取、常见失败场景逐个拆开说清楚,适合正在做 OpenHarmony 应用移植、又需要对网络层做联调排查的 Flutter 开发者参考。
1. 为什么在 OpenHarmony 上联调网络请求,比 Android 时代难这么多
1.1 原以为能直接复用的日志方案,在鸿蒙设备上却失灵了
刚开始做 Flutter for OpenHarmony 适配时,我和大多数人的想法一样:Flutter 本来就是跨平台的,网络请求用 Dio,日志用拦截器,换到 OpenHarmony 顶多改一下构建配置。真到调试阶段才发现,问题根本不在 Dart 层,而在 Dart 层下面的日志出口。
Android 上 Flutter 的 print、debugPrint 最终会进入 logcat,Android Studio 的 Logcat 面板能按 package 过滤,Dio 拦截器打印的内容会整整齐齐出现在过滤结果里。OpenHarmony 使用的日志系统是 hilog,它有自己的 domain、tag、级别概念,Flutter 引擎侧的日志走的是一条独立通道,平时用 hdc hilog 抓日志,默认情况下根本看不到 Flutter 的 print 输出。即使你用 hdc hilog | grep flutter 去硬捞,往往也只能看到引擎启动的几条初始化日志,Dio 拦截器的内容完全不见踪影。
这会带来一个很迷惑的现象:代码在模拟器上跑得好好的,控制台里各种网络请求日志按预期打印;同样一份代码编到 rk3568 真机上,接口照样能通、业务也正常,唯独请求日志像被吞掉一样。你会下意识认为是代码出问题了,其实只是输出通道换了,你的日志发到了另一个没人看的地方。
1.2 认清前提:OpenHarmony 上跑 Flutter 时,Dio 的底层网络栈并不可见
还有一个更值得先搞清楚的问题:Flutter for OpenHarmony 里发一个 HTTP 请求,底层到底是谁在处理?
在 Android 上,Dio 默认使用的 IOHttpClientAdapter 内部基于 dart:io 的 HttpClient,而这个类最终又回到平台侧执行。在 OpenHarmony 上,Flutter engine 为了适配这个平台,实现了独立的 dart:io 支撑层,网络请求实际会走 OpenHarmony 提供的能力。对应用层开发者来说,Dio 的代码几乎不用改,但链路已经变了,原来熟悉的 Socket、OkHttp 相关排查手段都不适用。
这个问题直接决定了我们后续怎么设计日志监控方案。如果还按 Android 时代的方式,把所有希望寄托在外部抓包工具或者平台侧网络日志上,在 OpenHarmony 上基本行不通,因为这些通路都不在原来的位置。真正可靠的做法是让日志在 Dart 层形成闭环——不管底层走的是什么通道,请求参数、响应状态、耗时这些数据都在 Dart 层有完整的可观测信息,你只要把这些信息通过可控制的通道输出出来,就能达到监控目的。
Pretty Dio Logger 恰好就是干这件事的。它不关心底层网络栈具体是谁,它只做一件事:在 Dio 发送请求和接收响应的生命周期里,把关键信息格式化并打印出来。所以我最终把它选定为 OpenHarmony 项目里的网络日志基础组件,而不是像以前一样靠系统日志面板。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Pretty Dio Logger 的工作机制拆解:拦截器如何把一次请求变成可读的报文
2.1 Logger 与 Dio 拦截器的协作流程
Pretty Dio Logger 本质上是 Dio 的一个 Interceptor,它被添加到 Dio 实例的拦截器链中。Dio 在每次请求时会按顺序执行拦截器,Pretty Dio Logger 在发送前拦截到 RequestOptions,从中读取 method、uri、headers、queryParameters、data 等字段;在收到响应后,它拿到 Response 对象,读取状态码、状态描述、响应头和响应体。
理解了这条链路,就明白了一个使用要点:拦截器添加的位置很重要。如果你在拦截器链靠前的地方加了重试、缓存之类的拦截器,再在最后加 Pretty Dio Logger,那么日志里体现的请求可能已经被前面的拦截器改写过,看到的数据未必是实际发到网络上的。我一般把日志拦截器放在最靠近 Dio 底层的位置,确保它是最后一个修改请求的拦截器,保证打印内容与真实网络请求一致。
Pretty Dio Logger 的核心类 PrettyDioLogger 提供了若干开关,配置方式如下:
dart复制import 'package:dio/dio.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
Dio dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
),
);
dio.interceptors.add(
PrettyDioLogger(
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
compact: false,
maxWidth: 90,
),
);
requestBody 和 responseBody 是调试期最关键的开关。建议 debug 环境全部打开,release 环境只保留 error 级别,避免把用户隐私数据打到日志文件里。compact: false 会让 JSON 以缩进格式输出,多花几行日志但可读性提升非常明显;maxWidth: 90 控制单行最大宽度,防止超长行在一些日志查看工具里被自动换行弄乱格式。
2.2 格式化输出背后藏着的实际价值
很多人在 Android 上见过 Pretty Dio Logger 的输出,觉得无非就是"给日志加点颜色和缩进",但放到 OpenHarmony 上,这种格式化带来的价值会被放大很多。
因为 hilog 的查看体验远不如 Android Studio 的 logcat,默认输出是一行接一行的原始文本,如果日志内容不结构化,在终端里基本无法阅读。Pretty Dio Logger 输出的请求方法、URL、状态码、耗时是分行且带标记的,即使在纯文本终端里也能快速定位关键信息。比如你只用一条命令 hdc hilog | grep "Dio",就能把该实例的所有网络日志聚在一起,再配合 keyboard_arrow_down 式的缩进结构,一眼就能看到哪一步有问题。
我还习惯在配置里加上 logPrint 自定义回调,把所有日志统一转发到自己封装的日志工具里,同时给每行加上时间戳:
dart复制PrettyDioLogger(
logPrint: (object) {
String text = object.toString();
if (text.startsWith('[') || text.startsWith('{')) {
// JSON 内容缩进打印
}
LogUtil.info('[NET] $text');
},
)
之所以要自己接管打印,是因为真机调试时经常需要把日志写入文件,再用 hdc file recv 拉出来慢慢分析,而默认的 print 输出不会自动写文件。后文会提到这个用途。
2.3 一个容易忽略的误区:日志 ≠ 网络报文
使用拦截器日志时心里要清楚一个边界:Pretty Dio Logger 打印的是 Dio 在 Dart 层组装出来的请求对象和解码后的响应对象,不是网络传输层的原始字节流。如果底层发生了代理转发、SSL 握手、重定向,这些信息在拦截器这一层要么看不到、要么只能通过错误对象间接推断。
在 OpenHarmony 上尤其要注意 HTTPS 证书校验类问题。比如开发阶段常见的 self-signed 证书错误,Dio 拦截器能捕获到异常,并打印出 HandshakeException 之类的信息,但具体是哪个证书链环节出了问题,还得再从别的通道获取原生侧日志才能完整判断。所以 Pretty Dio Logger 适合做"业务层级"的请求监控,用来确认参数对不对、返回结构对不对、耗时是否正常已经足够;如果要做"抓包级"的报文分析,它替代不了 PC 端抓包工具,这只是分工问题,不是工具缺陷。
3. 在 OpenHarmony 上接入前的关键准备:权限、日志通道与构建配置
3.1 先确认网络权限,否则一切日志都是"连接失败"
无论在 OpenHarmony 上把 Pretty Dio Logger 配得多好看,如果应用没有网络权限,请求会在最底层直接失败,而失败信息经过封装后,在拦截器里看起来就只是一个大而化之的异常。
OpenHarmony 应用默认不允许访问网络。与 Android 的 AndroidManifest.xml 不同,OpenHarmony 需要在 entry/src/main/module.json5 中声明权限,常见写法如下:
json5复制{
module: {
name: "entry",
type: "entry",
description: "$string:module_desc",
mainElement: "EntryAbility",
deviceTypes: ["phone", "tablet"],
requestPermissions: [
{
name: "ohos.permission.INTERNET",
}
],
}
}
这个 ohos.permission.INTERNET 属于 normal 级别权限,不需要用户动态授权,但不声明就完全断网。曾经遇到过几次奇怪的现象:用本地模拟器跑请求是成功的,日志里什么都正常,同一个包换到 rk3568 真机上,出现了 SocketException: Connection refused, errno = 13。这个 errno 13 是权限不足,而不是对端拒绝。后来检查才发现,编译产物里 module.json5 的权限声明被某次合并覆盖掉了。遇到莫名其妙的连接失败,先检查权限再去抠日志,这是第一条经验。
3.2 hilog 通道不通:解决 Flutter 日志找不到的问题
在 OpenHarmony 上做 Flutter 调试,最影响效率的问题之一就是日志看不到。我用 hdc hilog 抓设备日志时,默认几乎看不到 Dart 侧的 print 输出,这个问题不解决,日志工具配得再完善也没用。
当时实测下来,比较可靠的办法是给日志加上明确的 tag 前缀。Dio 拦截器打印的内容,在 OpenHarmony 进程的 hilog 输出里会归属到 Flutter 引擎所在的线程, tag 往往比较乱。如果用 grep 去过滤,默认搜不到。可以换一个思路:不让日志依赖平台通道,而是自己把日志写到文件里。
在 Flutter 层可以封装一个简单的文件日志器,在 Dart 侧将 PrettyDioLogger 的 logPrint 回调指向它,按天生成日志文件,文件存放在应用外部缓存目录。需要排查时,使用命令行工具把文件拉出来:
bash复制hdc shell "find /data/app -name '*net*.log' 2>/dev/null"
hdc file recv /data/app/el2/100/base/com.example.app/haps/entry/files/netlog_20250101.log ./netlog_20250101.log
注意实际路径会受应用沙箱路径规则影响,各版本可能不同,建议在应用里把日志文件绝对路径打印出来,拉取时直接使用该路径。这个方法在模拟器和真机上都能通用,比在终端里跟 hilog 搏斗可靠得多。
3.3 构建类型和混淆对日志的影响
OpenHarmony 环境下 Flutter 应用的构建产物,release 版本通常会开启压缩与混淆。Dart 层代码本身不太受传统混淆影响,但是日志输出经常被开发者在代码里用条件编译收敛掉。如果只在 debug 里添加了 PrettyDioLogger,release 版本出问题时想临时打开日志,却忘了改条件判断,就会浪费很长时间。
我的做法是在调试工具的初始化阶段显式区分构建模式:
dart复制bool enableNetLog = bool.fromEnvironment('ENABLE_NET_LOG', defaultValue: kDebugMode);
if (enableNetLog) {
dio.interceptors.add(PrettyDioLogger());
}
这样可以在需要时用 --dart-define=ENABLE_NET_LOG=true 覆盖默认值,不用改代码。实际项目中发布版默认关闭,线上问题需要复现时,再打一个开启日志的包定向排查,是成本最低的监控方式。
4. Flutter for OpenHarmony 集成 Pretty Dio Logger 的完整过程
4.1 工程结构与依赖准备
接入之前先把工程结构理清楚。标准的 Flutter for OpenHarmony 工程通常包含 ohos 目录作为 OpenHarmony 原生工程,lib 目录放 Dart 代码。在 pubspec.yaml 中加入依赖:
yaml复制dependencies:
dio: ^5.4.0
pretty_dio_logger: ^1.3.1
执行 flutter pub get 拉取依赖。需要留意 OpenHarmony 侧的 Flutter SDK 版本与 pub 上最新包可能存在的兼容问题。遇到原生编译报错时,先检查根目录的 oh-package.json5 以及 Flutter SDK 的 revision 是否匹配,再考虑依赖包升级,顺序不要本末倒置。
在开始之前最好明确目标设备型号。如果你手头是 rk3568 开发板,要确认内核里加载的是哪一套设备树,避免出现网络驱动未加载导致请求失败。这个和日志工具本身没有直接关系,但曾经有人拿着网络请求日志排查了半天,最后发现两个开发板启动加载的设备树不同、其中一个网络功能本身就不正常。板级问题先排除掉,再进入网络层调试。
4.2 封装统一 Dio 实例并挂载拦截器
我不希望每次请求临时创建 Dio,而是定义一个全局的网络管理器,把 Dio、拦截器、超时配置都收敛到一起。这样做后续切换测试环境、统一加 token 都方便。
dart复制class ApiClient {
ApiClient._internal() {
dio = Dio(
BaseOptions(
baseUrl: ApiConfig.baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 20),
sendTimeout: const Duration(seconds: 15),
headers: {'Accept': 'application/json'},
),
);
dio.interceptors.add(
PrettyDioLogger(
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
compact: false,
maxWidth: 90,
maxLineLength: 200,
logPrint: (object) => NetLog.write('[PRETTY_DIO] $object'),
),
);
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
String token = TokenStorage.getToken();
if (token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
),
);
}
static final ApiClient shared = ApiClient._internal();
late final Dio dio;
}
挂载顺序上有一个细节:我先加了 Pretty Dio Logger,再加了 token 注入拦截器。这样 token 注入会发生在日志拦截器之后,日志看到的是最终带着 Authorization 头的请求,整个链条上 token 有没有生效一目了然。如果把顺序反过来,日志会在 token 注入前打印,你看到请求头里没有 token,但实际发出的请求是有 token 的,排查时会做出错误判断,白白浪费时间。
4.3 在真机上用 hdc + hilog 确认日志是否真正输出
代码写完之后,需要确认日志通道真的通了。直接盲目写业务请求是不现实的,最稳妥的办法是先打一个简单请求验证。
在某个页面初始化时请求一个公开接口,例如:
dart复制Future<void> fetchHealth() async {
try {
Response<dynamic> resp = await ApiClient.shared.dio.get('/health');
if (resp.statusCode == 200) {
// 业务处理
}
} catch (e) {
// 错误处理
}
}
执行编译并部署到设备:
bash复制hdc install entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.example.app
hdc hilog | grep "PRETTY_DIO"
如果 grep 后没有任何输出,最可能的原因是文件日志器没有生效而不是 Dio 没发请求,此时优先去查看日志文件是否生成。真机环境下不该用控制台有没有输出作为唯一判断标准,文件里有没有内容才算数。
4.4 集成成功后应该看到的日志长什么样
正常情况下,日志文件里会形成类似下面的结构:
text复制[PRETTY_DIO] *** Request ***
[PRETTY_DIO] [method] GET
[PRETTY_DIO] [url] https://api.example.com/health
[PRETTY_DIO] [headers] {'Accept': 'application/json', 'Authorization': 'Bearer xxxx'}
[PRETTY_DIO] *** Response ***
[PRETTY_DIO] [statusCode] 200
[PRETTY_DIO] [duration] 312ms
[PRETTY_DIO] [data] {"code":0,"message":"ok"}
这个格式里最重要的是 duration。它直接告诉你一次请求从 Dart 层发出到响应返回用了多久。如果这个耗时小到几毫秒,基本可以判断命中了缓存,不是真实网络请求;如果耗时长到几秒,再结合具体接口做性能判断。我通常会维护一个耗时波动表,观察某些接口在特定网络环境下的耗时表现,某些 OpenHarmony 设备上经常出现首包 TLS 握手慢的问题,通过 duration 曲线能很快捕捉到这种规律。
5. 接入之后踩过的几个实际排查案例
5.1 现象一:请求日志偶尔吞掉后面的响应内容
集成后第一次完整测试就遇到一个奇怪问题:请求日志有时能打出来,响应日志有时只有一半,尤其是响应体比较长时,经常断在某个 JSON 字段中间。第一反应是 Dio 的 responseBody 解析出了问题,后来才发现是日志打印环节受限。
Flutter 引擎在 OpenHarmony 侧日志输出有单条长度限制,超长内容会被直接截断。Pretty Dio Logger 的 maxLineLength 只是控制换行逻辑,如果底层输出的单条日志超过 hilog 的单条限制,照样会被切掉。解决方法有两个方向:一是配置更小的 maxWidth,让每行短一些;二是在 logPrint 里自己按固定长度切分再逐条输出。我自己写了一个简单的分块逻辑:
dart复制void writeNetLog(String text) {
const int chunkSize = 800;
for (int i = 0; i < text.length; i += chunkSize) {
int end = (i + chunkSize < text.length) ? i + chunkSize : text.length;
LogUtil.info('[PRETTY_DIO] ${text.substring(i, end)}');
}
}
每次输出 800 字符,配合 maxWidth 就比较稳了,真机上没有再出现响应被吞。这是个在 Android 上不容易遇到的问题,但在 OpenHarmony 上几乎是必现的,需要当成默认风险来处理。
5.2 现象二:能打印请求信息,但响应错误信息全是空
还有一次排查接口报错,日志显示请求正常发出,响应状态码是 500,但响应体整个是空的。此时不该急着去怪服务端,要先确认日志拿到的是响应还是错误对象。Dio 在非 2xx 状态码时默认不会抛异常,响应体会原样放在 Response.data 里;但如果配置了 validateStatus 或者自定义了错误处理逻辑,某些错误响应可能会走 DioException 而不是 Response。
这时候要检查拦截器里的 error 分支。Pretty Dio Logger 本身会捕获 DioException,但如果你在前面加了自定义拦截器,并且提前把错误 handler.next 处理掉了,后续拦截器可能就看不到完整错误信息。这类问题在 Android 里不明显,因为 logcat 还能看到平台侧的部分网络信息,在 OpenHarmony 上则完全依赖 Dart 层日志,一旦拦截器顺序有问题,信息就彻底消失。
我后来在自定义错误拦截器里增加了兜底逻辑,保证所有错误都先打一份原始日志再往下传递。这样即使页面上的错误提示被吞掉,日志文件里也能找到根因,不会两眼一抹黑。
5.3 现象三:中文和特殊字符在日志里变成转义序列
服务端返回的 JSON 里有中文时,日志打印出来往往是 \u4f60\u597d 形式的转义序列。这不代表数据有问题,只是 Pretty Dio Logger 在序列化时选择了 ASCII 安全的方式,所有非 ASCII 字符都被转义了。
习惯之后你会发现这个特性在调试中反而有用:它可以避免日志文件因为编码问题变成乱码,尤其在 Windows 终端里查看日志时,不会被 GBK 编码问题干扰。真正需要看中文字符时,可以用在线工具把 \uXXXX 批量反转义,或者在自己封装的 logPrint 里对响应体特殊处理。数据本身没问题,别在这个上面纠结太久。
5.4 现象四:release 包日志突然全无
在打通了 debug 包的日志后,偶然需要排查 release 包问题,发现无论怎么操作,日志文件都只包含应用启动记录,Dio 请求日志完全没有。检查后发现是代码里无意间加了条件:
dart复制if (kDebugMode) {
dio.interceptors.add(PrettyDioLogger());
}
release 包不走这个分支,拦截器根本没进 Dio 实例。这个问题不算 OpenHarmony 特有,但值得提醒的是,OpenHarmony 的一些开发板在性能上偏弱,很多人会直接用 release 包测试功能,日志开关和构建模式强绑定时排查会很被动。用 bool.fromEnvironment 做映射,能让任何构建模式都可以按需开启日志,这条思路强烈建议提前落进代码里。
6. 实用性扩展:从日志工具走向网络监控闭环
Pretty Dio Logger 本身只是格式化输出,真正让它产生价值的,是把日志系统与业务监控打通。如果你的应用已经接入了 OpenHarmony 设备,日志不能只在本地文件里躺着,还要能主动暴露问题。
我在这套机制上做了小扩展:统计每个接口的请求耗时、成功率、HTTP 状态码分布,定时汇总到内存里,当耗时超过阈值时自动输出一条告警日志。这样不用人盯着日志文件,接口异常就会在日志中留下明确的标记。对于 Flutter for OpenHarmony 这种调试工具相对不完善的场景,这种"日志主动找人"的方式比每天翻文件高效得多。
核心代码其实很简单:
dart复制class ApiMonitor {
final Map<String, int> _errorCount = {};
final Map<String, double> _totalDuration = {};
void record(String path, int statusCode, int durationMs) {
if (statusCode >= 400) {
_errorCount[path] = (_errorCount[path] ?? 0) + 1;
}
_totalDuration[path] = (_totalDuration[path] ?? 0) + durationMs;
}
}
然后在 Dio 的统一响应拦截器里,对每个 Response 做记录,再把 ApiMonitor 接入 Pretty Dio Logger 的 logPrint,日志与监控指标就能在同一份输出中共存。这只是我按照项目需求做的一个小扩展,如果你项目的监控体系是完整的 APM,也可以直接把数据上报到自己的平台,思路是一样的。
dart复制// 响应拦截器示例
dio.interceptors.add(
InterceptorsWrapper(
onResponse: (response, handler) {
ApiMonitor.shared.record(
response.realUri.path,
response.statusCode ?? -1,
DateTime.now().difference(_startTimeMap[response.realUri.path] ?? DateTime.now()).inMilliseconds,
);
handler.next(response);
},
),
);
接入 Pretty Dio Logger 只是手段,不是目的。OpenHarmony 开发调试中遇到的网络问题,很多都不是 Dart 层代码本身的问题,但通过一套完善、可落地的日志体系,你能快速把"问题到底出在哪一层"这个问题先回答清楚。有了这个方向感,后续的排查才能以最高效率推进。在实际体验中,网络日志工具选型不需要贪多,如果团队已经在用Flutter + Dio这套组合,那么Pretty Dio Logger是投入产出比很高的选择,做好权限与日志通道初始化之后,它的价值在OpenHarmony上甚至比在Android上更容易被体感感受到——因为当别的调试手段都失灵的时候,一份完整的Dart层请求日志,往往就是你手里最后一张也最可靠的底牌。
