初识星闪:NearLink Kit不会凭空工作
很多人第一次打开华为 NearLink 的官方文档,会觉得这个功能挺简单:引入@kit.ConnectKit下的nearlink模块,调用discover方法,就能发现周围设备了。但实际写代码的时候,你会发现几个诡异的问题:
- 模拟器上什么也扫不到。
- 真机上好不容易扫到了,页面一退,回调就不走了。
- 明明手机都贴在一起了,就是发现不了。
HarmonyOS NearLink 服务不是蓝牙的一个变种,也不是 Wi-Fi Direct 的替换。它的底层机制、权限模型和生命周期管理,和传统短距通信有很大区别。这篇文章不讲官方文档已经有的那些“左对齐”的文字,直接从代码开始,带你跑通第一个能工作的星闪示例。
NearLink 解决什么问题
传统短距通信方案在 IoT 场景下有几个硬伤:
- Wi-Fi 直连:配对流程长,功耗高,设备数多了容易冲突。
- 经典蓝牙:带宽有限,不适合传图像或音频流。
- BLE:功耗低,但连接不稳定,有效传输距离短。
NearLink(星闪)结合了蓝牙的易用性和 Wi-Fi 的高吞吐,同时通过新的空口设计解决了多设备并发和低时延问题。
| 特性 | 传统 BLE | NearLink |
|---|---|---|
| 设备发现速度 | 秒级 | 百毫秒级 |
| 单连接传输 | 1~2 Mbps | 可达 20 Mbps |
| 并发设备数 | 有限(基站模式) | 可达数十个 |
| 时延 | 10ms+ | 微秒级 |
并不是所有场景都需要上 NearLink。如果你只是做一个简单的手机-手环通知同步,BLE 完全够用。但如果你遇到多设备同步、高带宽传输、低时延控制这类需求,NearLink 是更合适的选择。
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上 目标设备:至少两台真机(手机、平板均可,需支持星闪功能)注意:模拟器不支持星闪硬件能力,测试必须用真机。
核心实现:第一个能工作的星闪 Demo
我们先做一个最简单的功能:启动设备发现,并把扫描到的设备信息打印到日志里。
1. 权限声明(这一步很容易被跳过,但必须做)
在entry/src/main/module.json5中添加以下权限:
"requestPermissions": [ { "name": "ohos.permission.ACCESS_NEARLINK" }, { "name": "ohos.permission.ACCESS_FINE_LOCATION" }, { "name": "ohos.permission.ACCESS_BLUETOOTH" } ]其中ACCESS_NEARLINK是核心权限,ACCESS_FINE_LOCATION用于扫描周围的设备(这是系统级限制,不仅仅是为了定位),ACCESS_BLUETOOTH是为了兼容部分低版本设备。
这里需要注意:即使你的应用只在后台扫描,也必须主动申请定位权限。否则nearlink.discover的回调不会接收到任何设备信息,且不会抛出异常——这就是让人困惑的地方。
2. 编写发现模块
创建src/main/ets/model/NearLinkDiscover.ts,代码如下:
// NearLinkDiscover.ts// 负责启动和停止星闪设备发现,并通过回调返回结果import{nearbus}from'@kit.QosKit';// 注意:实际引入模块名可能与文档略有差异interfaceDiscoverResult{deviceName:string;deviceId:string;rssi:number;}exportclassNearLinkDiscover{privateisRunning:boolean=false;privateonDeviceFound:(device:DiscoverResult)=>void;privateonError:(err:string)=>void;constructor(onDeviceFound:(device:DiscoverResult)=>void,onError:(err:string)=>void){this.onDeviceFound=onDeviceFound;this.onError=onError;}startDiscovery():void{if(this.isRunning){console.warn('[NearLinkDiscover] already running');return;}this.isRunning=true;try{// 开始发现nearbus.discover({onDeviceFound:(device)=>{constresult:DiscoverResult={deviceName:device.deviceName||'Unknown',deviceId:device.deviceId,rssi:device.rssi??-100,};console.info(`[NearLinkDiscover] found device:${result.deviceName}, id:${result.deviceId}`);this.onDeviceFound(result);},onError:(error)=>{consterrMsg=`discover error:${error.code}, message:${error.message}`;console.error(`[NearLinkDiscover]${errMsg}`);this.onError(errMsg);this.isRunning=false;}});}catch(err){consterrMsg=`catch exception:${err.code},${err.message}`;console.error(`[NearLinkDiscover]${errMsg}`);this.onError(errMsg);this.isRunning=false;}}stopDiscovery():void{if(!this.isRunning){return;}try{nearbus.stopDiscover();console.info('[NearLinkDiscover] stopped');}catch(err){console.error(`[NearLinkDiscover] stop error:${err.code},${err.message}`);}finally{this.isRunning=false;}}isDiscovering():boolean{returnthis.isRunning;}}这段代码做了几件事:
- 封装了
nearbus.discover的调用,把原始回调包装成统一的DiscoverResult结构。 - 使用
isRunning状态避免重复启动。 - 在
onError回调中自动将状态重置为false,防止错误状态下状态不同步。
为什么不用直接在页面里写nearbus.discover?
实际项目里,设备发现通常不是一次性操作,而是和页面生命周期强相关。如果直接写在AboutToAppear里,离开页面时忘了调用stopDiscover,会导致后台持续扫描,增加功耗且可能引起系统层面的回调异常。
3. 在页面中使用
创建src/main/ets/pages/Index.ets:
import{NearLinkDiscover}from'../model/NearLinkDiscover';@Entry@Componentstruct Index{@StatediscoverState:string='未开始';@StatedeviceList:string[]=[];privatenearLinkDiscover:NearLinkDiscover=newNearLinkDiscover((device)=>{this.deviceList=[...this.deviceList,`${device.deviceName}(信号:${device.rssi})`];this.discoverState='发现中';},(err)=>{this.discoverState=`错误:${err}`;});build(){Column(){Text(`状态:${this.discoverState}`).fontSize(24).margin(20);Text(`已发现设备:${this.deviceList.length}`).fontSize(18);List({space:10}){ForEach(this.deviceList,(item:string)=>{ListItem(){Text(item).fontSize(16).padding(10);}},(item:string)=>item);}.layoutWeight(1).width('100%');Button('开始发现').onClick(()=>{if(!this.nearLinkDiscover.isDiscovering()){this.nearLinkDiscover.startDiscovery();this.discoverState='扫描中...';}});}.width('100%').height('100%');}aboutToDisappear():void{// 页面销毁时停止发现,防止内存泄漏和后台扫描this.nearLinkDiscover.stopDiscovery();}}关键点:
- 不要在
build()中创建 new NearLinkDiscover(),因为 build 会频繁触发,每触发一次就会注册一个新的回调对象,导致回调混乱。这里把它声明在private字段里,只实例化一次。 aboutToDisappear()里必须停掉发现,否则退出页面后回调仍然会触发,而此时@State引用的this已经可能无效,引起错误。
常见问题
问题 1:权限都已声明,但真机扫码无反应
现象:所有权限在module.json5里都加了,日志里不打印任何发现结果,也没有异常。
原因:ACCESS_FINE_LOCATION属于敏感权限,module.json5里声明后,还需要在运行时动态请求(对于 API 23+ 的目标设备,HarmonyOS NEXT 也遵循此规则)。如果用户没有在首次弹出时点“始终允许”,后续扫描不会返回设备。
解法:在页面aboutToAppear里,使用abilityAccessCtrl请求定位权限。
import{abilityAccessCtrl,common}from'@kit.AbilityKit';import{bundleManager}from'@kit.AbilityKit';requestLocationPermission():void{constcontext:common.UIAbilityContext=getContext(this)ascommon.UIAbilityContext;constatManager=abilityAccessCtrl.createAtManager();try{atManager.requestPermissionsFromUser(context,['ohos.permission.ACCESS_FINE_LOCATION']).then((data)=>{if(data.authResults[0]!==abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED){console.warn('[Permission] location not granted');}});}catch(err){console.error(`[Permission] request failed:${err.code}`);}}问题 2:页面返回后再进入,发现功能不工作
现象:第一次进入页面能正常发现设备,返回后再打开页面,设备列表为空,且日志里没有found device。
原因:nearbus.discover在同一个Ability实例中不允许重复调用。第一次退出时如果stopDiscover没有被正确调用(例如aboutToDisappear中忘记处理),系统会认为发现会话仍然存在,第二次调用discover会静默失败。
解法:务必在aboutToDisappear、页面显式退出(如点击返回键)以及错误回调中停掉发现。并且可以考虑在aboutToAppear中先调用一次stopDiscover以保证状态干净。
aboutToAppear():void{this.nearLinkDiscover.stopDiscovery();// 确保状态重置}最佳实践
不要在构建函数中创建长生命周期对象。
@State变量的变更会触发build()重新执行,如果每次重建都 new 一个NearLinkDiscover,会导致回调函数中的this指向错误实例,设备列表永远不会更新。设备列表使用不可变更新模式。在回调里写
this.deviceList = [...this.deviceList, newDevice],而不是直接push,因为@State只有在整体赋值时才触发重新渲染。直接修改数组内容不会更新 UI。在
onError中一定要重置状态。如果发现过程中出现错误(例如网络断开、系统资源不足),回调会收到错误。如果不重置isRunning = false,用户就无法再次启动发现,体验很差。
FAQ
Q:为什么真机上能发现设备,但连接不上?
A:本篇文章只覆盖了设备发现部分。连接操作需要额外的nearbus.connectAPI,且需要双方设备上都运行了支持 NearLink 的 service。后面会单独写一篇连接与数据通信的实战。
Q:模拟器上跑 code,但日志里没有任何输出?
A:模拟器没有星闪硬件,nearbus.discover会返回错误(错误码通常是-1或0,取决于版本)。必须用真机,且设备需支持星闪(目前仅少数旗舰机型)。
Q:为什么我一打开这个页面,手机就发热?
A:nearbus.discover默认会开启高频扫描,功耗较高。如果你不需要持续扫描,可以在发现到足够多的设备后手动调用stopDiscovery,或者使用scanInterval参数降低频率(该参数在部分 API 版本中支持)。
如果你也遇到类似问题,可以重点检查定位权限是否已明确授予,以及stopDiscovery是否在正确时机调用。官方文档对中断场景描述得比较少,建议结合实际运行效果一起验证。