《HarmonyOS技术精讲-NearLink Kit(星闪服务)》第2篇:设备发现——星闪设备发现机制与API详解
问题
HarmonyOS NEXT 开发里,startDiscovery这个 API 经常被误用。很多人第一次接触NearLink Kit的设备发现能力时,会发现官方示例能运行,但放到实际项目里就遇到各种问题:扫描结果重复、回调不触发、页面离开后扫描还在跑。这个功能本身不复杂,但真正麻烦的是生命周期管理和扫描结果的去重处理。
它解决什么问题
NearLink Kit是 HarmonyOS 提供的星闪通信服务框架。设备发现是星闪应用的第一步——扫描周边的星闪设备并获取设备的基础信息,比如设备名称、设备ID、信号强度等。
| 特性 | 传统蓝牙扫描 | 星闪设备发现 |
|---|---|---|
| 扫描模式 | 被动扫描 | 主动+被动混合 |
| 发现速度 | 慢(约3-5秒) | 快(约1-2秒) |
| 设备信息量 | 有限 | 更丰富 |
| 过滤能力 | 有限 | 支持多维度过滤 |
有两个典型场景适合用这个 API:
- 智能家居设备配网前扫描周边设备
- 会议系统发现附近的可投屏设备
不适合的场景:如果你只需要连接已知设备,直接使用连接 API 即可,没必要开启扫描。
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上 目标设备:手机核心实现
1. 准备工作:权限配置
在module.json5中添加权限声明。这一步容易漏掉,很多人直接跑示例发现没反应,原因就是权限没配。
{"module":{"requestPermissions":[{"name":"ohos.permission.NEARLINK_DISCOVERY","reason":"$string:permission_nearlink_discovery","usedScene":{"abilities":["EntryAbility"]}},{"name":"ohos.permission.INTERNET"}]}}注意:NEARLINK_DISCOVERY权限在 API 23 及以上版本才支持。如果跑在低版本设备上,这个权限不会生效,也不会报错,扫描就直接不回调了。
2. 发起设备发现扫描
这一段代码用于封装星闪发现的核心逻辑。实际项目里建议写成一个单独的服务类,不要直接把扫描逻辑塞进 UI 组件。
// NearLinkDiscoverService.etsimport{nearLink}from'@kit.ConnectivityKit';import{BusinessError}from'@kit.BasicServicesKit';classNearLinkDevice{name:string='';deviceId:string='';rssi:number=-100;manufacturerData:Uint8Array=newUint8Array();}interfaceDiscoverResult{devices:NearLinkDevice[];}exportclassNearLinkDiscoverService{privatediscoveryId:number=-1;privatediscoveredDevices:Map<string,NearLinkDevice>=newMap();privateonDeviceFound:((device:NearLinkDevice)=>void)|null=null;privateonError:((error:string)=>void)|null=null;// 注册发现监听registerListener(onFound:(device:NearLinkDevice)=>void,onError:(error:string)=>void):void{this.onDeviceFound=onFound;this.onError=onError;}// 启动发现startDiscovery():void{// 检查是否已经在扫描中if(this.discoveryId!==-1){console.warn('[NearLink] Discovery already running, skip');return;}// 构建发现回调constcallback:nearLink.IDiscoveryCallback={onError:(errCode:number,errMsg:string):void=>{console.error(`[NearLink] Discovery error:${errCode}-${errMsg}`);this.onError?.(`Discovery error:${errMsg}`);},onFound:(device:nearLink.DiscoveredDevice):void=>{// 防重复逻辑:用 deviceId 去重if(this.discoveredDevices.has(device.deviceId)){// 已存在的设备,更新信号强度constexisting=this.discoveredDevices.get(device.deviceId)!;existing.rssi=device.rssi;return;}// 新设备,加入列表并回调constnewDevice:NearLinkDevice={name:device.deviceName,deviceId:device.deviceId,rssi:device.rssi,manufacturerData:device.manufacturerData};this.discoveredDevices.set(device.deviceId,newDevice);this.onDeviceFound?.(newDevice);}};// 配置发现过滤条件(可选)constfilter:nearLink.DiscoveryFilter={type:nearLink.DiscoveryFilterType.DISCOVERY_FILTER_TYPE_MANUFACTURER,value:newUint8Array([0x01,0x02,0x03])// 示例制造商数据前缀};// 配置发现参数constconfig:nearLink.DiscoveryConfig={discoveryId:0,callback:callback,filter:filter// 不传 filter 则发现所有星闪设备};try{this.discoveryId=nearLink.startDiscovery(config);console.info(`[NearLink] Discovery started, id:${this.discoveryId}`);}catch(err){consterror=errasBusinessError;console.error(`[NearLink] startDiscovery failed:${error.code}-${error.message}`);this.onError?.(`startDiscovery failed:${error.message}`);}}// 停止发现stopDiscovery():void{if(this.discoveryId===-1){console.warn('[NearLink] No active discovery to stop');return;}try{nearLink.stopDiscovery(this.discoveryId);console.info('[NearLink] Discovery stopped');this.discoveryId=-1;// 清空缓存(根据业务需要决定是否清空)// this.discoveredDevices.clear();}catch(err){consterror=errasBusinessError;console.error(`[NearLink] stopDiscovery failed:${error.code}`);}}// 清除设备缓存clearCache():void{this.discoveredDevices.clear();}// 获取已发现的设备列表getDiscoveredDevices():NearLinkDevice[]{returnArray.from(this.discoveredDevices.values());}}关键细节:
- 防重复逻辑:
onFound回调会反复上报同一个设备,信号强度变化时也会再次回调。使用deviceId作为 Map 的 key 去重,只更新已存在设备的 RSSI,不重复添加。 - discoveryId 管理:
startDiscovery返回一个 id,停止时要传入这个 id。如果页面销毁时没有保存这个 id,后续无法正确停止扫描。 - filter 配置:不传 filter 会扫描周边所有星闪设备。如果只关心某个制造商的产品,用
DISCOVERY_FILTER_TYPE_MANUFACTURER过滤效率更高。
3. 在页面中使用
这段代码展示如何在 UI 组件中调用发现服务,并处理生命周期。
// IndexPage.etsimport{NearLinkDiscoverService,NearLinkDevice}from'./NearLinkDiscoverService';@Entry@Componentstruct IndexPage{@Statedevices:NearLinkDevice[]=[];@StateisScanning:boolean=false;privatediscoverService:NearLinkDiscoverService=newNearLinkDiscoverService();aboutToAppear():void{// 注册监听this.discoverService.registerListener((device:NearLinkDevice)=>{// 回调中更新 UI(@State 自动触发刷新)this.devices=this.discoverService.getDiscoveredDevices();},(error:string)=>{console.error(`[Page]${error}`);});}aboutToDisappear():void{// 页面离开时必须停止扫描,否则后台持续耗电this.stopScan();}startScan():void{this.isScanning=true;this.devices=[];this.discoverService.clearCache();this.discoverService.startDiscovery();}stopScan():void{this.isScanning=false;this.discoverService.stopDiscovery();}build(){Column(){Button(this.isScanning?'停止扫描':'开始扫描').onClick(()=>{this.isScanning?this.stopScan():this.startScan();}).margin(16)List(){ForEach(this.devices,(device:NearLinkDevice)=>{ListItem(){Row(){Text(device.name||'未知设备').fontSize(16)Text(`RSSI:${device.rssi}dBm`).fontSize(12).fontColor('#666')}.padding(12).width('100%')}})}.layoutWeight(1)}.width('100%').height('100%')}}注意aboutToDisappear中的stopScan()调用。实际项目里很多人漏掉这一步,导致页面返回后扫描还在持续,不仅耗电,还可能造成回调在已销毁页面中执行,引发异常。
4. 过滤条件完整示例
如果不希望扫描所有设备,可以用制造商数据过滤。这段代码展示如何只发现特定厂商的设备。
// 只发现制造商ID为 0x0001 的设备(示例)constfilter:nearLink.DiscoveryFilter={type:nearLink.DiscoveryFilterType.DISCOVERY_FILTER_TYPE_MANUFACTURER,value:newUint8Array([0x00,0x01])};// 或者使用设备服务类型过滤constfilterByService:nearLink.DiscoveryFilter={type:nearLink.DiscoveryFilterType.DISCOVERY_FILTER_TYPE_SERVICE,value:newUint8Array([0x10,0x20])// 示例服务 UUID 前缀};常见问题
问题 1:页面返回后扫描未停止
现象:从扫描页面返回,过一会儿发现手机还在发热,日志显示扫描回调仍在触发。
原因:aboutToDisappear中没有调用stopDiscovery。HarmonyOS 不会在页面销毁时自动停止已启动的扫描,需要开发者手动处理。
解决方案:严格在aboutToDisappear中停止扫描。如果服务类被多个页面共享,需要在服务类里判断是否有页面还在使用扫描,不要直接 stop。
问题 2:onFound 回调频繁触发同一设备
现象:一个设备在 3 秒内被回调 10 次,页面列表疯狂刷新。
原因:星闪设备会主动广播,每次收到广播都会触发回调。这不是 bug,而是星闪协议本身的特性。
解决方案:使用deviceId做去重,如上面的代码所示。注意:只更新 RSSI 等动态数据,不重复添加列表项。
最佳实践
不要把扫描逻辑直接写在 UI 组件里。扫描涉及状态管理、生命周期、回调处理,拆成独立服务类后测试和复用都更方便。
stopDiscovery 必须在页面销毁时调用。这不是可选的。如果忘记停止,扫描会持续消耗电量,而且回调中操作 UI 时可能因为页面已销毁而崩溃。
回调中更新 UI 时注意线程。
onFound运行在非 UI 线程,不要直接操作 UI 组件。ArkTS 的@State变量是线程安全的,赋值即可触发刷新。但如果有复杂的 UI 操作,建议使用setTimeout或postTask切回主线程。
FAQ
Q:为什么真机正常,模拟器不生效?
A:星闪能力依赖硬件支持,模拟器不具备星闪硬件,所以startDiscovery调用后不会触发任何回调。必须在 real device 上测试。
Q:为什么页面返回后状态丢失?
A:检查aboutToDisappear是否正确调用了stopDiscovery。另外,如果你在页面间切换时使用了NavStack,注意onPageShow和onPageHide的生命周期,建议在onPageHide中暂停扫描,onPageShow中恢复。
Q:过滤条件设置后,扫描不到设备怎么办?
A:先去掉 filter,测试是否能看到设备。如果能看见,说明 filter 配置错误。确认你的 filter value 和设备广播的制造商数据完全匹配(包括字节序和长度)。
如果你也遇到类似问题,可以重点检查生命周期和状态同步逻辑。不同设备上的星闪广播行为可能存在差异,建议真机测试。