《HarmonyOS技术精讲-蓝牙》基础篇:从零了解蓝牙服务开发
从零开始控制蓝牙:HarmonyOS 蓝牙开发基础
HarmonyOS NEXT 开发中,蓝牙功能是很多应用(智能家居、运动健康、文件传输)的基石。官方文档提供了完整的 API 描述,但许多初学者第一次接触bluetoothManager时,会发现一个尴尬的问题:照着示例写,代码能跑,但一引入项目就各种“水土不服”。最常见的是权限申请失败、扫描不到设备、或者页面跳转后状态丢失。
这篇文章的目标就是帮你解决这些问题。我们会从蓝牙的两种核心模式(传统蓝牙 BR/EDR 和低功耗蓝牙 BLE)出发,重点讲解如何通过 ArkTS 代码实现蓝牙开关、设备发现和扫描,并给出一个可以直接运行的完整示例。
蓝牙服务架构:先搞清楚你在用哪种
HarmonyOS 的蓝牙服务主要分为两套体系:BR/EDR(经典蓝牙)和 BLE(低功耗蓝牙)。它们不是二选一的关系,而是互补的。
| 特性 | 经典蓝牙 (BR/EDR) | 低功耗蓝牙 (BLE) |
|---|---|---|
| 典型场景 | 文件传输、音频播放(耳机、音箱)、蓝牙键盘鼠标 | 智能传感器、心率带、蓝牙信标、手环连接 |
| 功耗 | 高 | 极低 |
| 数据传输速率 | 高(~2 Mbps) | 低(~1 Mbps) |
| 连接建立速度 | 慢(几秒) | 快(毫秒级) |
| 核心 API | bluetoothManager.startBluetoothDiscovery | bluetoothManager.startBLEScan |
我的建议:如果你的应用只用于连接手环或获取传感器数据,直接选用 BLE。如果需要传输文件或连接音频设备,则必须使用经典蓝牙。在 HarmonyOS 中,这两类 API 的调用方式基本一致,但设备发现和连接后的操作截然不同。本文先覆盖最常用的设备发现部分。
环境说明
DevEco Studio 版本:DevEco Studio 5.0.1 (或更高版本) HarmonyOS SDK 版本:HarmonyOS 5.0.0 (或更高) 目标设备:HarmonyOS NEXT 真机(模拟器对蓝牙支持不完整)核心实现:从开关到发现设备
我们实现一个简单的页面:点击按钮开启蓝牙,然后扫描附近的设备,并把结果打印到日志中并用列表展示。
1. 权限配置
蓝牙操作涉及用户隐私,必须在module.json5中声明权限。
{"module":{"requestPermissions":[{"name":"ohos.permission.ACCESS_BLUETOOTH","reason":"$string:app_name","usedScene":{"abilities":["MainAbility"]}}]}}注意事项:如果只做 BLE 扫描,还需要声明ohos.permission.ACCESS_FINE_LOCATION(位置权限),因为 BLE 扫描可以用于定位。我见过很多人在模拟器上测试时发现扫描不到设备,最后发现是没加位置权限。
2. 引入蓝牙管理模块
import{bluetoothManager}from'@kit.ConnectivityKit';import{BusinessError}from'@kit.BasicServicesKit';3. 开关蓝牙
/** * 开启蓝牙 */enableBluetooth():void{try{// state: false 表示关闭,true表示开启// 注意:这个操作可能需要用户授权,但 API 本身会触发系统弹窗bluetoothManager.enableBluetooth();console.info('蓝牙开启成功');}catch(err){leterror=errasBusinessError;console.error(`蓝牙开启失败: code =${error.code}, message =${error.message}`);}}/** * 关闭蓝牙 */disableBluetooth():void{try{bluetoothManager.disableBluetooth();console.info('蓝牙关闭成功');}catch(err){leterror=errasBusinessError;console.error(`蓝牙关闭失败: code =${error.code}, message =${error.message}`);}}为什么这样写:直接调用enableBluetooth()即可,不需要额外操作。但要注意,如果在系统蓝牙的授权页面被用户拒绝,后续调用会抛异常。实际项目中,建议先检查当前蓝牙状态再操作。
4. 发现经典蓝牙设备
// 启动发现try{// 建议先检查蓝牙是否已开启if(!bluetoothManager.getState()){console.warn('蓝牙未开启,无法扫描');return;}bluetoothManager.on('bluetoothDeviceFind',(data:Array<string>)=>{console.info(`发现设备,设备地址列表:${JSON.stringify(data)}`);// data 是设备地址列表,需要通过 getRemoteDeviceName 获取名称this.deviceList=data.map((address)=>({address:address,name:bluetoothManager.getRemoteDeviceName(address)||'未知设备'}));});bluetoothManager.startBluetoothDiscovery();}catch(err){leterror=errasBusinessError;if(error.code===201){console.error('权限不足,请检查权限配置');}else{console.error(`扫描失败: code =${error.code}`);}}5. 发现低功耗蓝牙 (BLE) 设备
// 启动 BLE 扫描try{if(!bluetoothManager.getState()){console.warn('蓝牙未开启,无法扫描');return;}// 注册 BLE 设备查找回调bluetoothManager.on('BLEDeviceFind',(data:Array<bluetoothManager.ScanResult>)=>{console.info(`发现 BLE 设备,数量:${data.length}`);this.bleDeviceList=data.map((result)=>({address:result.deviceId,name:result.deviceName||'未知设备',rssi:result.rssi// 信号强度}));});// 开始扫描// 第二个参数是扫描过滤器,null 表示不过滤bluetoothManager.startBLEScan(null);console.info('BLE 扫描已启动');}catch(err){leterror=errasBusinessError;console.error(`BLE 扫描失败: code =${error.code}`);}关于过滤:startBLEScan的第二个参数可以用于过滤特定的服务 UUID,这对于连接特定设备非常有用。如果不过滤,可能会收到大量无关的设备广播,导致性能问题。
踩坑记录
坑1:页面返回后扫描状态丢失
现象:从发现设备的页面A跳转到其他页面,再返回A,之前的扫描回调不触发了。
原因:bluetoothManager.on()注册的回调是全局性的,但页面的生命周期(aboutToDisappear)导致状态引用丢失或回调被覆盖。
解决方案:在aboutToAppear注册,在aboutToDisappear注销。
aboutToAppear():void{bluetoothManager.on('BLEDeviceFind',this.onBLEDeviceFind);}aboutToDisappear():void{bluetoothManager.off('BLEDeviceFind',this.onBLEDeviceFind);// 停止扫描try{bluetoothManager.stopBLEScan();}catch(err){console.error('停止扫描失败');}}privateonBLEDeviceFind=(data:Array<bluetoothManager.ScanResult>)=>{// 处理数据};坑2:多次点击扫描按钮导致重复注册回调
现象:在同一个页面多次点击“开始扫描”,on('BLEDeviceFind')被调用多次,回调被触发了多次。
原因:on()方法多次调用不会覆盖之前的回调,而是会叠加。
解决方案:在每次注册前先注销旧的,或者用一个标志位控制。
privateisScanning:boolean=false;startScan():void{if(this.isScanning){console.warn('扫描正在进行中');return;}this.isScanning=true;bluetoothManager.on('BLEDeviceFind',this.onBLEDeviceFind);bluetoothManager.startBLEScan(null);}stopScan():void{if(!this.isScanning){return;}bluetoothManager.off('BLEDeviceFind',this.onBLEDeviceFind);bluetoothManager.stopBLEScan();this.isScanning=false;}最佳实践
- 不要在
build()中注册回调。build()可能会在状态变化时被多次调用,频繁注册/注销on()不仅会浪费性能,还会引起回调混乱。应该在aboutToAppear或onPageShow中注册。 - 使用合适的数据结构存储设备列表。蓝牙设备扫描是增量式的,同一个设备可能被多次发现。建议使用
Map<string, DeviceInfo>来存储,键是设备地址,值包含设备信息和发现时间。这样可以避免重复渲染 UI。 - 及时清理资源。即使页面销毁时没有强制停止扫描,蓝牙资源也不会立刻被回收。在
aboutToDisappear中主动调用stopBLEScan()和off()是保证稳定性的好习惯。
Demo 入口
一个完整的页面组件,集成了上面所有点。
@Entry@Componentstruct BluetoothDemo{@StatedeviceList:Array<{name:string,address:string}>=[];@StatebleDeviceList:Array<{name:string,address:string,rssi:number}>=[];privateisScanning:boolean=false;aboutToAppear():void{// 注册生命周期的回调bluetoothManager.on('bluetoothDeviceFind',this.onBREDeviceFind);bluetoothManager.on('BLEDeviceFind',this.onBLEDeviceFind);}aboutToDisappear():void{this.stopBLEscan();this.stopBREScan();bluetoothManager.off('bluetoothDeviceFind',this.onBREDeviceFind);bluetoothManager.off('BLEDeviceFind',this.onBLEDeviceFind);}privateonBREDeviceFind=(data:Array<string>)=>{this.deviceList=data.map((address)=>({address:address,name:bluetoothManager.getRemoteDeviceName(address)||'未知设备'}));};privateonBLEDeviceFind=(data:Array<bluetoothManager.ScanResult>)=>{this.bleDeviceList=data.map((result)=>({address:result.deviceId,name:result.deviceName||'未知设备',rssi:result.rssi}));};build(){Column(){Button('开启蓝牙').onClick(()=>{bluetoothManager.enableBluetooth();})Button('扫描经典蓝牙').onClick(()=>{this.startBREScan();})Button('扫描 BLE 设备').onClick(()=>{this.startBLEscan();})Button('停止扫描').onClick(()=>{this.stopBREScan();this.stopBLEscan();})Text('经典蓝牙设备')List(){ForEach(this.deviceList,(item:{name:string,address:string})=>{ListItem(){Text(`${item.name}(${item.address})`)}})}Text('BLE 设备')List(){ForEach(this.bleDeviceList,(item:{name:string,address:string,rssi:number})=>{ListItem(){Text(`${item.name}(${item.address}) [信号:${item.rssi}]`)}})}}.width('100%').height('100%').padding(20)}privatestartBREScan():void{try{bluetoothManager.startBluetoothDiscovery();console.info('经典蓝牙扫描开始');}catch(err){console.error(`经典蓝牙扫描失败:${err}`);}}privatestopBREScan():void{try{bluetoothManager.stopBluetoothDiscovery();console.info('经典蓝牙扫描停止');}catch(err){console.error(`停止经典蓝牙扫描失败:${err}`);}}privatestartBLEscan():void{if(this.isScanning){return;}this.isScanning=true;try{bluetoothManager.startBLEScan(null);console.info('BLE 扫描开始');}catch(err){console.error(`BLE 扫描失败:${err}`);this.isScanning=false;}}privatestopBLEscan():void{try{bluetoothManager.stopBLEScan();console.info('BLE 扫描停止');}catch(err){console.error(`停止 BLE 扫描失败:${err}`);}this.isScanning=false;}}FAQ
Q:为什么真机可以扫描到设备,模拟器上却不行?
A:模拟器对蓝牙硬件的支持有限,蓝牙扫描和连接必须在真机上测试。模拟器主要用于 UI 和业务逻辑验证。
Q:为什么startBluetoothDiscovery返回成功,但bluetoothDeviceFind回调一直没有被触发?
A:检查两台设备是否开启了蓝牙且可被发现。对于经典蓝牙,需要确保设备在配对模式下。另外,如果多次调用了startBluetoothDiscovery而没有stop,回调可能会被阻塞。
Q:扫描到设备后,为什么getRemoteDeviceName()返回空字符串?
A:这在经典蓝牙扫描中比较常见。部分设备在广播阶段不会暴露名称,必须建立连接后才能获取。建议使用设备地址作为唯一标识。
Q:扫描列表里为什么会有重复设备?
A:BLE 设备会周期性广播,每次广播都可能触发BLEDeviceFind。使用Map以设备地址为 key 去重是最简单的处理方式。
这篇文章覆盖了 HarmonyOS 蓝牙开发的基础——如何控制蓝牙开关、如何发现设备。下一篇文章会深入讲解如何与设备建立连接并进行数据交互。如果你也遇到了类似的生命周期或权限问题,希望这篇内容能给你提供一些参考。