news 2026/7/19 3:31:21

HarmonyOS技术精讲-Connectivity Kit:Wi-Fi扫描与连接实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS技术精讲-Connectivity Kit:Wi-Fi扫描与连接实战

开篇:一个常见的Wi-Fi开发误区

很多人在HarmonyOS NEXT里第一次接触Wi-Fi扫描时,会直接按照官方示例写代码,然后发现:扫描结果出来了,但连接总是失败。更头疼的是,失败后的错误回调描述不够详细,导致调试成本很高。

这个问题在HarmonyOS开发里比较常见。Connectivity Kit 的Wi-Fi模块设计思路和Android有差异,但更贴近系统底层状态机。如果不对API的生命周期和状态同步做处理,很容易出现“扫描正常,连接异常”的情况。

这篇实战文章会从权限声明开始,逐步实现一个完整的Wi-Fi扫描与连接功能。重点是wifiManager的获取、startScanonScanStateChange的配合、connectToNetwork的正确调用方式,以及扫描列表的UI渲染。

Connectivity Kit 解决什么问题

Connectivity Kit(短距通信服务)是HarmonyOS提供的无线连接能力集合。其中的Wi-Fi模块主要负责:

  • 扫描周围可用的Wi-Fi热点
  • 建立或断开Wi-Fi连接
  • 监听Wi-Fi状态变化(连接、断开、信号强度变化)

适合场景:

场景说明
智能家居配网手机扫描局域网设备热点,完成配网
办公设备联网设备需要连接到企业Wi-Fi
网络诊断工具扫描周边AP,分析信道占用和信号覆盖

不适合场景:

  • 后台长期扫描:系统对高频Wi-Fi扫描有节流机制,不建议在后台持续调用。
  • 隐藏网络扫描:API未直接暴露被动监听隐藏SSID的能力(需要用户主动输入SSID)。

和“应用内发Intent跳转系统设置”的方式相比,直接使用Connectivity Kit API的优点是用户可以完全在应用内完成操作,不需要跳转到系统页面,体验更流畅。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上 目标设备:手机

核心实现

1. 权限声明

Wi-Fi扫描和连接需要两个敏感权限:ohos.permission.GET_WIFI_INFOohos.permission.SET_WIFI_INFO。前者用于读取Wi-Fi状态,后者用于连接Wi-Fi。

entry/src/main/module.json5中添加:

{"module":{"requestPermissions":[{"name":"ohos.permission.GET_WIFI_INFO","reason":"用于获取Wi-Fi状态和扫描结果","usedScene":{"abilities":["EntryAbility"]}},{"name":"ohos.permission.SET_WIFI_INFO","reason":"用于连接指定的Wi-Fi网络","usedScene":{"abilities":["EntryAbility"]}}]}}

注意:在HarmonyOS NEXT上,权限声明是静态的,但运行时仍然需要动态请求(部分权限在弹窗后自动授予,这里两个权限都属于“用户授权”类型)。

2. 获取wifiManager对象

wifiManager是 Wi-Fi 模块的入口对象。不能直接new,必须通过connect接口获取。

import{wifiManager}from'@kit.ConnectivityKit';import{common}from'@kit.AbilityKit';// 获取wifiManager对象functiongetWifiManager(context:common.Context):wifiManager.WifiDevice{// 这里传入的context必须是UIAbility的context,不能是页面组件的contextreturnwifiManager.getWifiDevice(context);}

官方文档没有强调context的类型限制。如果传入的是this.getUIContext()中获取的组件context,会导致wifiManager.getWifiDevice返回undefined

3. 扫描与监听

扫描调用startScan,但结果不是直接返回的,需要通过onScanStateChange监听扫描完成事件。

// 发起扫描functionstartWifiScan(wifi:wifiManager.WifiDevice):void{// startScan没有返回值,扫描结果通过回调获取wifi.startScan();}// 监听扫描状态变化functiononScanStateChange(wifi:wifiManager.WifiDevice,callback:(state:boolean)=>void):void{wifi.on('scanStateChange',(state:boolean)=>{// state为true表示扫描成功,false表示扫描失败或超时callback(state);});}// 获取扫描结果functiongetScanResults(wifi:wifiManager.WifiDevice):Array<wifiManager.WifiScanInfo>{// 必须在扫描完成后调用,否则返回空数组returnwifi.getScanInfoResults();}

注意:onScanStateChange回调是一次性的吗?不是。它会在每次扫描完成时触发。如果多次调用startScan,回调会被多次触发。建议在页面aboutToDisappear时调用wifi.off('scanStateChange')取消监听。

4. 构建Wi-Fi配置并连接

连接Wi-Fi需要构建一个WifiDeviceConfig对象,然后调用connectToNetwork

import{wifiManager}from'@kit.ConnectivityKit';functionconnectToWifi(wifi:wifiManager.WifiDevice,ssid:string,preSharedKey:string):void{// 构建Wi-Fi配置letconfig:wifiManager.WifiDeviceConfig={ssid:ssid,preSharedKey:preSharedKey,securityType:wifiManager.SecurityType.WPA2_PSK// 根据实际情况选择};// 调用连接方法wifi.connectToNetwork(config,(error,data)=>{if(error){console.error(`连接失败:${JSON.stringify(error)}`);return;}console.info(`连接成功,netId:${data}`);});}

安全类型枚举值:

枚举对应加密方式
WPA2_PSKWPA2个人网
WPA3_SAEWPA3个人网
OPEN开放网络
WEPWEP加密(不推荐)

如果不知道目的AP的加密方式,可以先扫描,从WifiScanInfocapabilities字段中提取。基本做法是解析capabilities字符串,匹配WPAWPA2WPA3等关键字。

5. 完整页面实现

下面是一个完整的Index.ets页面组件,包含扫描列表展示和连接按钮交互。

import{wifiManager}from'@kit.ConnectivityKit';import{common}from'@kit.AbilityKit';import{promptAction}from'@kit.ArkUI';@Entry@Componentstruct Index{@StatescanResults:Array<wifiManager.WifiScanInfo>=[];@StateselectedSsid:string='';@Statepassword:string='';@StateisScanning:boolean=false;privatewifi:wifiManager.WifiDevice|undefined=undefined;aboutToAppear():void{// 在UIAbility中使用this.context获取context// 注意:页面组件的context和UIAbility的context不同,这里假设PageAbility中已处理好// 实际项目中建议将wifiManager对象的获取放在UIAbility中,通过全局状态管理letcontext=getContext(this)ascommon.BaseContext;this.wifi=wifiManager.getWifiDevice(context);}// 执行扫描doScan():void{if(!this.wifi){promptAction.showToast({message:'Wi-Fi管理器获取失败'});return;}this.isScanning=true;// 先清除上一次的扫描结果this.scanResults=[];// 注册扫描状态监听this.wifi.on('scanStateChange',(state:boolean)=>{this.isScanning=false;if(state){this.scanResults=this.wifi!.getScanInfoResults();promptAction.showToast({message:`扫描完成,发现${this.scanResults.length}个网络`});}else{promptAction.showToast({message:'扫描失败'});}});// 发起扫描this.wifi.startScan();}// 连接到选中的网络connect():void{if(!this.wifi){promptAction.showToast({message:'Wi-Fi管理器获取失败'});return;}if(!this.selectedSsid){promptAction.showToast({message:'请先选择网络'});return;}// 构建配置,安全类型暂时固定为WPA2,实际可根据capabilities解析letconfig:wifiManager.WifiDeviceConfig={ssid:this.selectedSsid,preSharedKey:this.password,securityType:wifiManager.SecurityType.WPA2_PSK};this.wifi.connectToNetwork(config,(error,data)=>{if(error){promptAction.showToast({message:`连接失败:${error.message}`});return;}promptAction.showToast({message:`连接成功,netId:${data}`});});}build(){Column(){// 操作按钮区域Row(){Button('扫描Wi-Fi').onClick(()=>this.doScan()).width('45%')Button('连接').onClick(()=>this.connect()).width('45%')}.padding(10).width('100%').justifyContent(FlexAlign.SpaceBetween)// 扫描结果列表List(){ForEach(this.scanResults,(item:wifiManager.WifiScanInfo,index:number)=>{ListItem(){Row(){Column(){Text(item.ssid).fontSize(16).fontWeight(FontWeight.Bold)Text(`信号强度:${item.rssi}dBm`).fontSize(12).fontColor(Color.Gray)}.alignItems(HorizontalAlign.Start).layoutWeight(1)if(this.selectedSsid===item.ssid){Text('已选中').fontSize(14).fontColor(Color.Green)}}.padding(10).width('100%').onClick(()=>{this.selectedSsid=item.ssid;})}.divider({strokeWidth:1,color:'#e0e0e0'})})}.width('100%').layoutWeight(1)// 密码输入区域if(this.selectedSsid){Row(){Text('密码:')TextInput({placeholder:'输入Wi-Fi密码'}).onChange((value:string)=>{this.password=value;})}.padding(10).width('100%')}}.width('100%').height('100%')}}

这段代码的核心逻辑:

  1. aboutToAppear中初始化wifiManager对象。
  2. doScan方法先清除旧结果,注册监听,再调用startScan
  3. connect方法使用选中网络的SSID和用户输入密码构建配置,调用connectToNetwork
  4. 列表项onClick选中一个网络,选中后显示密码输入框。

注意事项:

  • 生命周期问题:页面销毁时必须取消监听。可以在aboutToDisappear中调用this.wifi.off('scanStateChange'),否则下次进入页面会重复注册,导致多次回调。
  • 状态同步:扫描是异步的,扫描完成前获取扫描结果会返回空数组。所以用isScanning状态控制UI反馈。
  • UI刷新scanResults@State变量,扫描完成后赋值会自动触发列表刷新。

踩坑记录

坑1:onScanStateChange不回调

现象:调用startScan后,onScanStateChange回调完全不触发。

原因:HarmonyOS对Wi-Fi扫描有节流逻辑。如果在短时间内(约30秒)连续调用startScan,后一次调用会被系统忽略,且不会触发任何回调。这其实是系统层面的频率限制,官方文档没有明确说明。

解决方案:在每次扫描前判断距离上次扫描的时间间隔。如果小于30秒,提示用户稍后再试,或者使用计时器控制扫描频率。实测稳定间隔是40秒以上。

坑2:connectToNetwork回调顺序不稳定

现象:调用connectToNetwork后,回调被立刻触发,但实际连接还没完成(比如还没出现Wi-Fi图标)。特别是网络密码错误时,回调依然返回成功。

原因connectToNetwork的异步回调只是表示“连接指令已下发”,并不代表“连接已成功建立”。真正判断连接是否成功,需要监听wifiManager.on('connectionChange')事件。官方示例里直接用回调判断成功,是一个常见的误导,这个设计本身有点反直觉。

解决方案

// 连接成功后,等待连接状态变化wifi.on('connectionChange',(state:wifiManager.ConnectionState)=>{if(state===wifiManager.ConnectionState.CONNECTED){console.info('Wi-Fi连接成功');// 此时可以获取当前连接信息}elseif(state===wifiManager.ConnectionState.DISCONNECTED){console.info('Wi-Fi断开');// 可能是密码错误导致主动断开}});

最佳实践

  1. 使用单例管理wifiManager对象:不要在每次操作前都调用getWifiDevice。如果页面销毁后重新创建,会导致旧监听失效。推荐在UIAbility中初始化,通过AppStorage或全局变量传递给页面。

  2. 利用秒级Listen频率降低回调漏接概率on('scanStateChange')在扫描完成时触发一次,但如果扫描开始后页面立即跳转,可能错过回调。可以在aboutToAppear中注册,页面内确保监听生命周期与页面一致,不跨页面传递。

  3. 扫描结果列表展示前清理上一次结果getScanInfoResults返回的是全量缓存结果,包括上一次扫描的数据。如果不清理,列表会显示过时的Wi-Fi。每次扫描前将scanResults置空,等待回调后再赋值。

FAQ

Q1:为什么真机上能扫描到网络,模拟器上扫描不到?

A:模拟器对Wi-Fi硬件的模拟不完整,getScanInfoResults在模拟器上始终返回空数组。这是预期行为,Wi-Fi相关功能建议使用真机调试。

Q2:密码输入正确却连接失败,错误信息是“INVALID_PARAM”?

A:检查WifiDeviceConfig中的securityType是否和实际AP匹配。最常见的是扫描到的网络是开放网络(OPEN),但代码里写了WPA2_PSK。建议根据WifiScanInfo.capabilities动态解析安全类型。

Q3:应用退出后,Wi-Fi连接会断开吗?

A:调用connectToNetwork后,Wi-Fi连接是由系统管理的,应用退出不会自动断开。如果需要主动断开,可以调用wifi.disconnect方法。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/19 3:31:08

Dify工作流实现自动化研究报告生成技术解析

1. 项目概述&#xff1a;Dify工作流实现深度研究自动化这个基于Dify平台构建的Deep Researcher工作流&#xff0c;本质上是一个自动化研究报告生成系统。我在实际部署测试中发现&#xff0c;它最惊艳的地方在于能够将原本需要数小时的人工研究过程&#xff0c;压缩到5分钟内完成…

作者头像 李华
网站建设 2026/7/19 3:30:13

nginx的web服务器配置

1&#xff0c;创建多个ip和进入/etc/hosts添加域名信息&#xff0c;在测试能不能获取2&#xff0c;创建文件夹存放网页文件index.html3&#xff0c;进入/etc/nginx/conf.d 创建zhangsan和lisi的配置文件 具体内容如下4&#xff0c;获取zhangsan和lisi的页面数据

作者头像 李华
网站建设 2026/7/19 3:29:44

Jenkins持续集成与部署(CI/CD)实战指南

1. Jenkins持续集成服务器概述Jenkins是一个开源的持续集成和持续交付(CI/CD)工具&#xff0c;它基于Java开发&#xff0c;能够帮助开发团队自动化构建、测试和部署软件项目。作为一个可扩展的自动化服务器&#xff0c;Jenkins既可以作为简单的CI服务器使用&#xff0c;也可以成…

作者头像 李华
网站建设 2026/7/19 3:29:36

AM62L CBASS防火墙实战:从寄存器配置到内存安全策略

1. 从手册到实战&#xff1a;理解AM62L CBASS防火墙的核心价值 如果你正在基于德州仪器&#xff08;TI&#xff09;的AM62L Sitara处理器开发产品&#xff0c;尤其是在汽车电子、工业自动化或者高安全性的物联网设备领域&#xff0c;那么“系统安全”绝对是你绕不开的核心议题。…

作者头像 李华
网站建设 2026/7/19 3:29:16

GraphRAG图创建与检索实战:解决向量检索无法处理的关系推理问题

1. 项目概述&#xff1a;当知识图谱遇上检索增强&#xff0c;我们到底在优化什么&#xff1f;GraphRAG这个词最近在技术圈里被反复提起&#xff0c;但很多人一看到“Graph”就下意识觉得是搞图数据库的&#xff0c;一看到“RAG”又自动联想到向量检索和大模型问答——结果两边都…

作者头像 李华
网站建设 2026/7/19 3:29:09

Python开发环境搭建:从解释器安装到PyCharm配置完整指南

在实际学习和工作中&#xff0c;Python 开发环境的搭建是每个初学者和开发者必须迈出的第一步。一个稳定、高效的开发环境不仅能提升编码体验&#xff0c;更能避免因环境问题导致的各类诡异错误。本文将围绕 Python 解释器的安装、PyCharm IDE 的配置与激活&#xff0c;提供一个…

作者头像 李华