ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鸿蒙网络状态监控与管理实战:API拆解与工具封装

鸿蒙网络状态监控与管理实战:API拆解与工具封装 去年年底我把一个跨平台工具应用往鸿蒙上迁移第一版上线后收到最多的反馈就是“为什么我在电梯里打开应用就白屏”“为什么切到 5G 后一直加载不出来”。排查到最后根因全在网络状态的处理上——应用压根不知道当前网络是否可用、是什么类型、有没有发生切换。那以后我认真把鸿蒙的网络状态监控与管理捋了一遍沉淀了一套可以直接上项目的方案。这篇博文就把我的实现思路、核心代码和踩过的坑完整分享出来适合正在开发鸿蒙应用、或者打算从 Android/iOS 往鸿蒙迁移的同学直接参考。1. 为什么要认真做网络状态监控先看我踩过的坑1.1 从一次白屏事故说起我接手过一个资讯类应用页面加载依赖网络请求。最初版本只在启动时检查一次网络是否连接结果用户在真实场景里频繁反馈“打开就是白屏”。复现后发现问题出在三个场景用户从 Wi-Fi 切换到蜂窝数据应用没有感知网络已重建旧请求全部超时页面数据一直停留在空状态。用户进了电梯网络短暂断开系统已经自动切到蜂窝但应用还认为自己在 Wi-Fi 环境下缓存策略完全错乱。部分平板设备同时存在以太网和 Wi-Fi系统选择了以太网作为默认网络应用却只监听了 Wi-Fi 状态导致状态判断直接错误。这些问题的共性是应用没有建立“网络状态全链路监控”只是在一个时间点做了静态判断。网络状态是一个动态过程从可用到不可用、从 Wi-Fi 到蜂窝、从网络能力变化到 DNS 变化每个节点对业务的影响都不同。如果应用不能感知这些变化用户体验就会出现各种“灵异现象”。1.2 网络监控到底要解决哪几类问题在鸿蒙里做网络状态监控本质上要回答四个问题当前有没有网络——判断 isConnected这是最基础的一层。当前是什么类型的网络——Wi-Fi、蜂窝、以太网还是其他不同类型对应不同的流量策略和展示逻辑。当前网络是否能真正访问互联网——有网络不代表能上网可能是无 Internet 能力的受限网络。网络状态什么时候发生了变化——这是做自动重试、缓存切换、提示更新的核心触发点。前三个是快照查询第四个是持续监听。一个完整的网络监控组件必须把快照和监听两条线都打通。只看快照容易漏掉变化只监听不查快照会导致初始化阶段的状态缺失。两者结合才能覆盖绝大多数业务场景。1.3 鸿蒙网络框架的几个基本概念鸿蒙的网络模块经历了版本演进。早期核心模块是ohos.net.connection负责连接管理、状态查询和回调注册。从 API 12 开始系统把更多网络管理能力收敛进kit.NetworkKit下的 networkManager 模块connection 模块的职责逐渐聚焦到“连接状态”本身。这套设计与 Android 的 ConnectivityManager 思路相似但也有鸿蒙特色NetHandle网络句柄标识一个具体的网络类似 Android 的 Network 对象。NetCapabilities网络能力集描述网络类型蜂窝、Wi-Fi、以太网以及是否具备 Internet 能力。NetConnection网络连接对象通过它注册回调来监听网络变化。bearerTypes承载类型数组标识当前网络走的是哪类链路。理解这几个概念后面看代码就不会发懵。2. 核心 API 拆解connection 模块就是网络监控的主战场2.1 权限声明与模块引入少一步都跑不通网络状态查询在鸿蒙里属于普通权限不需要像定位那样动态弹窗申请。在module.json5的 requestPermissions 里加上下面这条即可{ module: { requestPermissions: [ { name: ohos.permission.GET_NETWORK_INFO } ] } }模块引入时注意版本差异。我的项目用的是 API 12 及以上的 SDK写法如下import connection from ohos.net.connection; import { BusinessError } from ohos.base;如果项目适配的是旧版 SDK路径可能还是ohos.net.connection不带版本差异但这个模块一直比较稳定我实测下来从 API 9 到 API 15 的基本调用方式保持一致。唯一要注意的是如果同时用了 Kit 的kit.NetworkKit和ohos.net.connection避免在同一个文件里混用否则可能出现同名接口的类型冲突。2.2 获取当前默认网络getDefaultNet 不是万能的第一步通常是通过 getDefaultNet 拿到当前系统的默认网络句柄connection.getDefaultNet().then((netHandle: connection.NetHandle) { console.info(默认网络句柄: JSON.stringify(netHandle)); }).catch((err: BusinessError) { console.error(获取默认网络失败: JSON.stringify(err)); });这里有一个新手特别容易踩的坑getDefaultNet 成功返回并不代表当前一定能上网。系统在某些场景下会把一个没有 Internet 能力的网络设为默认网络比如酒店 Wi-Fi 的登录页网络或者企业内网。所以拿到 netHandle 后必须继续验证网络能力。注意getDefaultNet 在无网络的环境下会走 catch 分支错误码通常是 2300003找不到满足条件的网络。处理时不要把“无网络”和“获取失败”混为一谈建议把 catch 里的无网络情况显式映射成状态枚举。2.3 拿到网络能力与连接属性判断“能上网”而不是“连着网”判断一个网络是否真正可用最可靠的办法是拿它的 NetCapabilities。function checkNetworkCapabilities(netHandle: connection.NetHandle): Promiseboolean { return connection.getNetCapabilities(netHandle).then((caps: connection.NetCapabilities) { // bearerTypes 数组比如 [1] 表示 Wi-Fi[2] 表示蜂窝 console.info(承载类型: JSON.stringify(caps.bearerTypes)); // netCaps 数组包含网络能力位 let hasInternet caps.netCaps.indexOf(connection.NetCap.NET_CAPABILITY_INTERNET) ! -1; console.info(是否具备Internet能力: hasInternet); return hasInternet; }).catch((err: BusinessError) { console.error(获取网络能力失败: JSON.stringify(err)); return false; }); }除了网络能力连接属性里还藏着 IP、DNS、网关这些关键信息。业务上做“当前 IP 展示”“内网判断”时非常有用。connection.getConnectionProperties(netHandle).then((props: connection.ConnectionProperties) { // 网络接口名比如 wlan0、eth0 let interfaceName: string props.interfaceName; // IP 地址列表 let linkAddresses: Arrayconnection.LinkAddress props.linkAddresses; // 路由信息 let routes: Arrayconnection.RouteInfo props.routes; // DNS 服务器 let dnsServers: Arraystring props.dnsServers; console.info(接口名: ${interfaceName}); console.info(DNS: ${JSON.stringify(dnsServers)}); }).catch((err: BusinessError) { console.error(获取连接属性失败: JSON.stringify(err)); });有一点要提醒linkAddresses 里包含 IPv4 和 IPv6 地址而且可能同时存在多个。展示给用户的时候要过滤掉链路本地地址否则会出现“拿到了 169.254.x.x 还当成正规 IP”的尴尬。2.4 注册网络状态变更监听四种回调的触发时机连接状态查询只是静态快照真正让监控“活”起来的是连接回调。鸿蒙的 NetConnection 提供了四个常用事件netAvailable网络可用但这个“可用”只代表连接建立不代表具备 Internet 能力。netCapabilitiesChange网络能力发生变化比如从无 Internet 变为有 Internet。netConnectionPropertiesChange网络连接属性变化比如 IP、DNS、网关变了。netLost网络丢失连接断开。注册监听的标准流程是先 createNetConnection再 register然后逐个挂 on 回调最后还要主动调用 register 的 Promise 或者 catch 来处理注册失败。let netConnection: connection.NetConnection connection.createNetConnection(); netConnection.register((err: BusinessError) { if (err) { console.error(注册网络监听失败: JSON.stringify(err)); return; } console.info(网络监听注册成功); }); netConnection.on(netAvailable, (netHandle: connection.NetHandle) { console.info(网络可用: JSON.stringify(netHandle)); // 在这里做一次完整的全网状态查询 }); netConnection.on(netCapabilitiesChange, (netHandle: connection.NetHandle) { console.info(网络能力变化); }); netConnection.on(netConnectionPropertiesChange, (netHandle: connection.NetHandle) { console.info(网络连接属性变化); }); netConnection.on(netLost, (netHandle: connection.NetHandle) { console.info(网络丢失: JSON.stringify(netHandle)); });这里的触发时机非常误导人我单独列一张表说明回调事件触发含义业务上建议做什么netAvailable网络链路建立了查询详细状态不要直接发大请求netCapabilitiesChange网络能力变了重新判断是否具备 Internet 能力netConnectionPropertiesChangeIP/DNS/网关变了刷新网络详情、清理局部缓存netLost网络彻底断开暂停请求、显示离线状态、触发重连逻辑用完记得注销监听避免内存泄漏和重复回调。我一般把注销放在页面 onPageHide 或者组件销毁时执行。netConnection.unregister((err: BusinessError) { if (err) { console.error(注销网络监听失败: JSON.stringify(err)); } });3. 手把手封装一个 NetworkMonitor 工具组件3.1 数据模型与状态枚举设计直接裸调 API 写业务代码会越来越乱。我建议先封装一个单例的 NetworkMonitor对外统一暴露查询和订阅接口。第一步定义枚举和数据模型export enum NetworkType { NONE 0, WIFI 1, CELLULAR 2, ETHERNET 3 } export interface NetworkState { isConnected: boolean; hasInternet: boolean; networkType: NetworkType; typeName: string; ipAddress: string; dnsServers: string[]; netHandleId: number; }为什么要封装成这样的模型因为业务方不关心底层 NetHandle 和 bearerTypes 的具体数值他们只需要“有没有网、什么网、能不能上网、IP 是什么”。把这些字段规整好UI 层和业务层就不需要再和鸿蒙底层 API 打交道。3.2 核心代码实现封装类的大体结构如下import connection from ohos.net.connection; import { BusinessError } from ohos.base; export class NetworkMonitor { private static instance: NetworkMonitor | null null; private netConnection: connection.NetConnection | null null; private state: NetworkState { isConnected: false, hasInternet: false, networkType: NetworkType.NONE, typeName: 无网络, ipAddress: , dnsServers: [], netHandleId: -1 }; private observers: Array(state: NetworkState) void []; static getInstance(): NetworkMonitor { if (NetworkMonitor.instance null) { NetworkMonitor.instance new NetworkMonitor(); } return NetworkMonitor.instance; } async refresh(): PromiseNetworkState { try { let netHandle: connection.NetHandle await connection.getDefaultNet(); let caps: connection.NetCapabilities await connection.getNetCapabilities(netHandle); let props: connection.ConnectionProperties await connection.getConnectionProperties(netHandle); this.state.isConnected true; this.state.netHandleId netHandle.netId; // 判断承载类型 if (caps.bearerTypes.indexOf(connection.NetBearType.BEARER_WIFI) ! -1) { this.state.networkType NetworkType.WIFI; this.state.typeName Wi-Fi; } else if (caps.bearerTypes.indexOf(connection.NetBearType.BEARER_CELLULAR) ! -1) { this.state.networkType NetworkType.CELLULAR; this.state.typeName 蜂窝数据; } else if (caps.bearerTypes.indexOf(connection.NetBearType.BEARER_ETHERNET) ! -1) { this.state.networkType NetworkType.ETHERNET; this.state.typeName 以太网; } else { this.state.networkType NetworkType.NONE; this.state.typeName 其他网络; } // 判断 Internet 能力 this.state.hasInternet caps.netCaps.indexOf(connection.NetCap.NET_CAPABILITY_INTERNET) ! -1; // 提取 IP 和 DNS let ipv4 props.linkAddresses.filter((addr: connection.LinkAddress) { return addr.address.indexOf(:) -1; }); this.state.ipAddress ipv4.length 0 ? ipv4[0].address : ; this.state.dnsServers props.dnsServers; return this.state; } catch (err) { this.state.isConnected false; this.state.hasInternet false; this.state.networkType NetworkType.NONE; this.state.typeName 无网络; this.state.ipAddress ; this.state.dnsServers []; return this.state; } } async start(): Promisevoid { if (this.netConnection) { return; } await this.refresh(); this.netConnection connection.createNetConnection(); await new Promisevoid((resolve, reject) { this.netConnection!.register((err: BusinessError) { if (err) { reject(err); } else { resolve(); } }); }); this.netConnection.on(netAvailable, async () { await this.refresh(); this.notifyObservers(); }); this.netConnection.on(netCapabilitiesChange, async () { await this.refresh(); this.notifyObservers(); }); this.netConnection.on(netLost, async () { this.state.isConnected false; this.state.hasInternet false; this.state.networkType NetworkType.NONE; this.state.typeName 无网络; this.notifyObservers(); }); } async stop(): Promisevoid { if (!this.netConnection) { return; } this.netConnection.unregister(); this.netConnection null; } getCurrentState(): NetworkState { return { ...this.state }; } addObserver(observer: (state: NetworkState) void): void { this.observers.push(observer); } removeObserver(observer: (state: NetworkState) void): void { let index this.observers.indexOf(observer); if (index ! -1) { this.observers.splice(index, 1); } } private notifyObservers(): void { let snapshot { ...this.state }; this.observers.forEach((observer) { observer(snapshot); }); } }这套封装有几个细节值得展开讲第一refresh 里把 getDefaultNet、getNetCapabilities、getConnectionProperties 三个异步调用串起来了。这样每次状态刷新拿到的都是同一时刻的网络快照避免数据不一致。如果你先查连接再查能力中间网络切换了两个结果可能来自不同网络UI 上就会出现“显示有网络但 IP 是旧的”这种自相矛盾。第二netLost 回调里直接重置状态不依赖 refresh。原因很简单网络已断开时再去 getDefaultNet 只会拿到错误码没有必要多一次无效调用。把状态直接置为离线响应更快。第三getCurrentState 返回的是浅拷贝对象防止外部直接篡改内部状态。这个习惯在多人协作的项目里特别重要。3.3 在页面中集成与 UI 联动封装好之后页面里的使用就非常干净了。以 ArkUI 页面为例我一般这样写Entry Component struct NetworkStatusPage { State currentState: NetworkState { isConnected: false, hasInternet: false, networkType: NetworkType.NONE, typeName: 无网络, ipAddress: , dnsServers: [], netHandleId: -1 }; aboutToAppear(): void { let monitor NetworkMonitor.getInstance(); monitor.addObserver((state: NetworkState) { this.currentState state; }); monitor.start(); } aboutToDisappear(): void { let monitor NetworkMonitor.getInstance(); monitor.removeObserver(this.updateState); // 注意如果多个页面共用这个实例不要在页面销毁时就 stop等应用退出再 stop } build() { Column({ space: 12 }) { Text(当前状态: ${this.currentState.typeName}) .fontSize(20) Text(IP 地址: ${this.currentState.ipAddress}) Text(是否可上网: ${this.currentState.hasInternet ? 是 : 否}) Text(DNS: ${this.currentState.dnsServers.join(, )}) } .padding(20) } }这里有个非常重要的问题单例的 NetworkMonitor 被多个页面共享时stop 的时机要全局把控。我曾经在某个页面 onPageHide 里直接调了 stop结果从页面 A 跳到页面 B监听全没了B 页面完全感知不到网络变化。我的经验是start 只在 App 入口处调用一次stop 放在应用退出时。页面层面只做 addObserver/removeObserver不动底层监听。这样生命周期清晰也不会出现重复注册。3.4 集成到跨端移植应用的注意事项如果你是从 Electron 或 Tauri 应用移植到鸿蒙大概率会碰到一个典型问题原来 Web 端的 navigator.onLine 判断完全不可用Web 端习惯用 online/offline 事件但鸿蒙应用是原生事件驱动网络状态变化需要主动订阅而不是等事件冒泡。我移植一个下载工具时把原本基于navigator.onLine的逻辑全部替换成了 NetworkMonitor 订阅同时注意了三点Web 端只区分“在线/离线”鸿蒙里要额外区分“有网但不可上网”否则受限网络下会一直重试请求。Web 端的navigator.connection能拿到 downlink 和 effectiveType鸿蒙的 connection 模块不直接提供带宽估算但可以通过 netCapabilities 里的网络详情做间接判断。跨端代码里凡是直接引用 window 对象的逻辑都会崩需要先做环境判断或抽象出一层网络状态接口让不同平台各自实现。这一步做得好不好直接影响应用在不同平台上的行为一致性。移值后一定不要只验证功能流程网络弱网下的行为也要专门做一轮测试。4. 常见问题与排查技巧实录4.1 权限与版本兼容问题最容易出的问题反而不是权限而是权限之外的逻辑错误。有同事把GET_NETWORK_INFO当成了受限权限在代码里写了动态申请结果发现系统压根不弹窗还白白增加了一堆权限判断代码。记住这个权限声明即生效不需要运行时授权。版本兼容方面我更常遇到的是 API 变更导致的编译失败。比如某些旧版本教程里会用connection.NetBearType.BEARER_WIFI新版本 SDK 里类型名保持一致但模块路径从ohos.net.connection换成了 Kit 风格。我的建议是项目里统一用一个常量或者工具类集中封装网络模块的 import后续升级 SDK 时只改一个文件而不是满项目找调用点。4.2 收不到回调多半是生命周期没理清我在项目里遇到过一次“完全收不到 netLost”的情况。排查了半天发现是页面在 aboutToAppear 里注册监听但同一个页面在 aboutToDisappear 里调用了 stop 去注销底层 connection。后续页面再进来时start 因为this.netConnection已经被置空又重新创建了但实际上前面一次 stop 已经把它整个搞乱了。经过那次踩坑我固化了一个原则注册了必须对应注销但“注销回调”和“注销底层连接”是两回事。页面的 removeObserver 只移观察者集合底层 NetConnection 的生命周期由全局单例统一管理。谁负责注册谁负责注销边界必须清晰。4.3 用无线调试和抓包工具验证网络切换很多开发者在模拟器上测试网络监控结果一切正常一到真机就出问题。模拟器的网络环境太干净Wi-Fi 和蜂窝模拟不出真实的切换竞争。我的调试建议是用真机开启无线调试配合抓包工具来验证切换场景。无线调试在鸿蒙设备上的开启路径是开发者选项里打开“无线调试”然后用 hdc 命令连接。这样一边跑应用一边看日志切换网络时能实时看到 netAvailable、netCapabilitiesChange、netLost 的触发顺序。抓包工具可以配置代理来模拟断网场景把代理关掉请求立刻失败此时观察应用是否能在几秒内感知网络不可用。具体做法是在电脑上运行抓包工具设备 Wi-Fi 设置里填上电脑 IP 和代理端口然后反复切换代理开关。代理关闭那一刻应用里的请求会超时NetworkMonitor 应该能配合请求失败触发一次完整的状态刷新。如果应用没有报错也没有提示说明监控逻辑没有覆盖到“请求失败后主动刷新状态”这条链路。注意抓包代理只用于本地开发调试不要在生产应用里内置任何代理逻辑。调试完务必关掉代理并恢复手机网络设置。4.4 排查案例电梯场景下的网络恢复之前提到的电梯白屏问题定位过程很典型。日志显示应用在电梯里发出请求时网络刚从 Wi-Fi 切到蜂窝但 UI 层拿到的还是旧状态所以没有触发重试。我加了两个修复点一是在请求失败回调里主动调用NetworkMonitor.getInstance().refresh()重新拉一次最新状态。二是 netCapabilitiesChange 回调里比对变化前后的网络类型一旦发现类型变化立刻通知业务层清理正在进行的请求队列。修复后测试人员在电梯里反复进出了十几次应用都能在网络恢复后 2 秒内自动重新加载内容没有再出现白屏。这个案例也验证了一件事网络监控不是只做被动感知它必须和业务的重试机制联动才能真正解决用户体验问题。5. 网络管理从“看到状态”到“管理状态”5.1 networkManager 能做什么监控只解决了“知道发生了什么”管理则是“主动控制状态”。鸿蒙从 API 12 开始引入 networkManager 模块能力比 connection 更侧重控制面。我常用到的接口包括import { networkManager } from kit.NetworkKit; // 获取当前默认网络 let defaultNet await networkManager.getDefaultNetwork(); // 获取所有网络 let allNets await networkManager.getAllNets(); // 关闭某个网络接口例如断开 Wi-Fi 让系统切换到蜂窝 await networkManager.setNetworkInterfaceActive(wlan0, false);系统会根据网络评分自动选择最优网络这背后逻辑类似“网络优选”综合信号强度、网速、误码率给每个网络打分默认使用分数最高的。普通应用不需要干预这个选择但某些特殊场景需要主动管理。比如企业定制应用里要求以太网优先于 Wi-Fi或者在测试工具里需要强制切换网络类型来验证功能。这时 networkManager 的控制接口就派上了用场。5.2 结合业务场景的管理策略网络管理的经典场景是“自动下载策略”。我的下载模块里会先订阅 NetworkMonitor 状态然后根据网络类型决定下载行为Wi-Fi 环境下全速下载不限制并发数。蜂窝数据环境下提示用户“当前使用流量网络”默认暂停大文件下载。无网络或不可上网停止所有请求只保留最基本的 UI 渲染。这套策略写起来不复杂但关键是“执行时机”。如果只在启动时判断一次用户在下载途中切换网络就不会被正确处理。我是在每次网络状态刷新后重新评估一遍策略并记录当前策略版本号切换时把未完成任务暂存等网络恢复后自动续传。另一个容易被忽略的联动点是缓存策略。网络类型变化时之前缓存的 CDN 资源可能失效。我在网络切换回调里增加了缓存目录切换逻辑Wi-Fi 用高质量缓存流量网络用低质量缓存这样既保证了体验又控制了流量开销。5.3 车载与多设备场景的扩展鸿蒙在车机上的应用越来越多车载场景的网络管理思路和手机不太一样。车机同时存在蜂窝、Wi-Fi、以太网连接车联网盒子甚至多路专用链路。这种环境下单纯依赖系统默认网络选择是不够的因为不同的业务对网络的要求不同地图导航需要低延迟媒体播放需要大带宽OTA 更新则希望仅在特定条件下进行。我在车载项目里做了一层“业务分级网络调度”底层同样是用 connection 查询状态、用 networkManager 做接口控制上层按业务维度分配网络优先级。这套思路和 AutoSAR 网络管理里的网络状态协调机制有相似之处关注网络模式的切换、唤醒和休眠以及多 ECU 之间的网络协调。不管底层实现差异多大“先感知再决策再控制”这个思路是共通的。多设备协同场景则是鸿蒙的独特优势。手机、平板、智慧屏组成超级终端后网络资源可以共享。某个设备网络不佳时应用可以通过分布式软总线感知组网内其他设备的网络能力并把关键请求分发到网络更好的设备上。这种体验在传统移动平台很难做到但对开发者来说意味着网络层不仅要处理本机状态还要考虑组网状态。现阶段可以先从本机监控做起后续再往分布式方向演进。我在实际项目中体会最深的一点是网络监控没有银弹一定要先搞清楚你的业务对“网络状态”的敏感点在哪里。有的应用只需要知道“有没有网”有的应用必须知道“IP 变没变”有的应用在乎的是网络类型切换后缓存是否失效。理清需求再决定用哪几个接口、监听哪几个回调方案自然就清晰了。如果一上来就把所有回调全挂上、把日志打到飞起最后只会陷入状态风暴反而抓不住重点。最后再分享一个小技巧给 NetworkMonitor 增加一个可选的日志开关线上发布时关闭详情日志只保留状态变更摘要。排查网络类问题时日志就是破案的关键线索但线上环境日志过多又会拖累性能。开关做进去开发和排障都能省不少力气。
返回列表