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