1. 为什么选择uniAPP开发低功耗蓝牙应用如果你正在开发智能手环、智能家居设备这类需要与手机交互的硬件产品uniAPP绝对是个不错的选择。我做过十几个蓝牙相关的项目发现uniAPP的跨平台特性确实能省不少事——一套代码可以同时跑在iOS和Android上再也不用为两个平台分别开发了。低功耗蓝牙BLE和传统蓝牙最大的区别就是省电。我实测过一个智能手环项目用BLE连接手机每天同步3次数据续航能达到7天而用传统蓝牙协议同样条件下只能用3天。BLE特别适合那些不需要持续传输大量数据的场景比如心率监测、计步器、智能门锁等。uniAPP对BLE的支持相当完善。从设备搜索到连接建立从服务发现到数据读写整个流程的API都封装得很好。不过在实际项目中我发现有些坑需要注意比如Android和iOS在蓝牙权限处理上的差异不同手机厂商对BLE协议栈的实现细节等。后面我会结合具体代码告诉你如何避开这些坑。2. 开发前的准备工作2.1 环境搭建首先确保你的开发环境已经就绪。我推荐使用HBuilderX作为开发工具它针对uniAPP做了很多优化。创建一个新的uniAPP项目后需要在manifest.json里配置蓝牙权限{ mp-weixin: { requiredBackgroundModes: [bluetooth], permission: { scope.bluetooth: { desc: 需要蓝牙权限 } } } }对于Android 6.0以上的设备还需要动态申请位置权限因为BLE扫描需要用到位置服务。这个坑我踩过——明明蓝牙权限都有了就是搜不到设备后来发现是没开位置权限。解决方法是在页面加载时检查权限// 检查位置权限 uni.authorize({ scope: scope.userLocation, success() { console.log(已授权位置权限) }, fail() { uni.showModal({ title: 提示, content: 需要位置权限才能搜索蓝牙设备, showCancel: false }) } })2.2 蓝牙适配器初始化所有蓝牙操作的前提是初始化蓝牙适配器。这里有个细节要注意在iOS上如果用户拒绝蓝牙权限或者手机蓝牙硬件不可用初始化会直接失败而在Android上系统可能会自动弹出权限请求。uni.openBluetoothAdapter({ success: (res) { console.log(蓝牙适配器初始化成功) this.startDiscovery() }, fail: (err) { console.error(初始化失败, err) if (err.errCode 10001) { uni.showToast({ title: 蓝牙适配器不可用, icon: none }) } } })初始化成功后建议立即监听蓝牙适配器状态变化。我在项目中遇到过用户中途关闭蓝牙导致功能异常的情况加上这个监听就能及时处理uni.onBluetoothAdapterStateChange((res) { console.log(蓝牙适配器状态变化: ${res.available ? 可用 : 不可用}) if (!res.available) { uni.showToast({ title: 蓝牙已关闭, icon: none }) } })3. 设备搜索与连接实战3.1 高效搜索蓝牙设备开始搜索前建议先停止之前的搜索如果有的话并清空设备列表。我遇到过设备重复显示的问题就是因为没做这个清理uni.startBluetoothDevicesDiscovery({ success: (res) { console.log(开始搜索) // 监听新发现的设备 uni.onBluetoothDeviceFound((res) { const devices res.devices // 过滤掉无效设备 const validDevices devices.filter(device device.name device.deviceId ) // 去重处理 this.deviceList [...new Set([...this.deviceList, ...validDevices])] }) }, fail: (err) { console.error(搜索失败, err) } })搜索到目标设备后建议手动停止搜索以节省电量。我一般设置15秒的自动停止setTimeout(() { uni.stopBluetoothDevicesDiscovery({ success: () { console.log(已停止搜索) } }) }, 15000)3.2 稳定连接建立技巧连接设备时最常见的错误是10003连接失败。根据我的经验这通常是因为设备已经连接但状态没更新或者设备不在可连接状态。解决方法是在连接前先检查状态createBLEConnection(deviceId) { if (this.connecting) { uni.showToast({ title: 已有设备连接, icon: none }) return } uni.showLoading({ title: 连接中... }) uni.createBLEConnection({ deviceId, success: () { uni.hideLoading() console.log(连接成功) this.getServices(deviceId) }, fail: (err) { uni.hideLoading() console.error(连接失败, err) // 自动重试逻辑 if (err.errCode 10003 this.retryCount 3) { this.retryCount setTimeout(() { this.createBLEConnection(deviceId) }, 500) } } }) }连接成功后不要立即获取服务我发现在Android上延迟1秒左右更稳定setTimeout(() { this.getBLEDeviceServices(deviceId) }, 1000)4. 服务发现与数据交互4.1 理解蓝牙服务架构BLE设备采用GATT通用属性规范数据按层级组织服务Service设备提供的功能集合特征Characteristic服务中的具体数据点描述符Descriptor特征的额外信息获取服务列表后需要找到目标服务UUID。这里有个技巧打印所有服务然后根据设备文档找到对应的服务uni.getBLEDeviceServices({ deviceId, success: (res) { console.log(所有服务:, res.services) // 假设我们需要电池服务 const batteryService res.services.find( s s.uuid.toLowerCase() 0000180f-0000-1000-8000-00805f9b34fb ) if (batteryService) { this.getCharacteristics(deviceId, batteryService.uuid) } } })4.2 特征值操作详解特征值有三种主要属性read可读取write可写入notify可订阅通知获取特征值时要检查属性是否支持你要的操作uni.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) { const charList res.characteristics charList.forEach(char { if (char.properties.read) { // 可读取的特征 this.readChar char.uuid } if (char.properties.write) { // 可写入的特征 this.writeChar char.uuid } if (char.properties.notify) { // 可订阅的特征 this.notifyChar char.uuid this.enableNotify(deviceId, serviceId, char.uuid) } }) } })启用通知是个关键步骤很多同学在这里卡住。必须在成功启用后才能收到设备推送的数据enableNotify(deviceId, serviceId, characteristicId) { uni.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId, state: true, success: () { console.log(通知启用成功) // 必须在这里设置监听 uni.onBLECharacteristicValueChange((res) { console.log(收到数据:, this.ab2hex(res.value)) }) } }) }4.3 数据读写最佳实践写入数据时要注意格式转换。BLE通信使用ArrayBuffer但业务代码通常操作的是普通数组或字符串。我封装了几个转换函数// 字符串转ArrayBuffer str2ab(str) { const buf new ArrayBuffer(str.length) const bufView new Uint8Array(buf) for (let i 0; i str.length; i) { bufView[i] str.charCodeAt(i) } return buf } // 16进制字符串转ArrayBuffer hex2ab(hex) { const typedArray new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h parseInt(h, 16))) return typedArray.buffer } // ArrayBuffer转16进制字符串 ab2hex(buffer) { return Array.prototype.map.call( new Uint8Array(buffer), bit (00 bit.toString(16)).slice(-2) ).join() }写入数据示例sendCommand(cmd) { const buffer this.hex2ab(cmd) uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.writeChar, value: buffer, success: () { console.log(写入成功) }, fail: (err) { console.error(写入失败, err) } }) }5. 连接管理与异常处理5.1 完善的连接状态管理BLE连接容易受环境影响断开需要做好状态管理。我建议维护以下状态变量connecting是否正在连接connected是否已连接deviceId当前连接设备IDserviceId主服务IDcharacteristics特征值集合监听连接状态变化uni.onBLEConnectionStateChange((res) { this.connected res.connected if (!res.connected) { console.log(连接断开) this.reconnect() // 自动重连逻辑 } })5.2 常见错误处理根据我的经验这些错误最常见10001蓝牙适配器不可用 → 检查蓝牙是否开启10003连接失败 → 检查设备是否可连接尝试重试10004找不到服务 → 确认服务UUID是否正确10005找不到特征值 → 确认特征UUID是否正确实现一个统一的错误处理器handleBLEError(err) { console.error(BLE错误:, err) const map { 10001: 蓝牙不可用请检查蓝牙设置, 10003: 连接失败请重试, 10004: 服务不存在, 10005: 特征值不存在 } uni.showToast({ title: map[err.errCode] || 未知错误: ${err.errCode}, icon: none }) // 特定错误自动恢复 if (err.errCode 10001) { setTimeout(() { uni.openBluetoothAdapter() // 尝试重新初始化 }, 1000) } }5.3 资源释放页面卸载时一定要释放资源否则可能导致下次连接失败onUnload() { if (this.deviceId) { uni.closeBLEConnection({ deviceId: this.deviceId }) } uni.stopBluetoothDevicesDiscovery() // 移除所有监听 uni.offBluetoothAdapterStateChange() uni.offBLEConnectionStateChange() uni.offBLECharacteristicValueChange() }6. 实战案例智能手环数据同步以智能手环为例演示完整的数据交互流程。假设手环提供以下服务电池服务0x180F读取电量设备信息服务0x180A读取固件版本自定义服务0xFFE0同步运动数据6.1 读取设备信息async readDeviceInfo() { // 读取电池电量 const battery await this.readCharacteristic( 0000180f-0000-1000-8000-00805f9b34fb, 00002a19-0000-1000-8000-00805f9b34fb ) console.log(当前电量:, battery.getUint8(0)) // 读取固件版本 const version await this.readCharacteristic( 0000180a-0000-1000-8000-00805f9b34fb, 00002a26-0000-1000-8000-00805f9b34fb ) console.log(固件版本:, this.ab2str(version)) } readCharacteristic(serviceId, characteristicId) { return new Promise((resolve, reject) { uni.readBLECharacteristicValue({ deviceId: this.deviceId, serviceId, characteristicId, success: (res) { resolve(res.value) }, fail: reject }) }) }6.2 同步运动数据// 启用运动数据通知 enableSportDataNotify() { uni.notifyBLECharacteristicValueChange({ deviceId: this.deviceId, serviceId: 0000ffe0-0000-1000-8000-00805f9b34fb, characteristicId: 0000ffe1-0000-1000-8000-00805f9b34fb, state: true, success: () { uni.onBLECharacteristicValueChange((res) { const data this.parseSportData(res.value) console.log(步数:, data.steps) console.log(心率:, data.heartRate) }) } }) } // 解析运动数据 parseSportData(buffer) { const view new DataView(buffer) return { steps: view.getUint16(0, true), heartRate: view.getUint8(2) } }6.3 发送控制命令// 设置时间 setDeviceTime() { const now new Date() const buffer new ArrayBuffer(6) const view new DataView(buffer) view.setUint16(0, now.getFullYear(), true) view.setUint8(2, now.getMonth() 1) view.setUint8(3, now.getDate()) view.setUint8(4, now.getHours()) view.setUint8(5, now.getMinutes()) uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: 0000ffe0-0000-1000-8000-00805f9b34fb, characteristicId: 0000ffe2-0000-1000-8000-00805f9b34fb, value: buffer }) }7. 性能优化与调试技巧7.1 连接优化策略在弱信号环境下我发现这些策略很有效增加连接超时Android默认是30秒实现指数退避重试缓存设备信号强度RSSI优先连接信号强的设备// 获取设备信号强度 getRssi(deviceId) { return new Promise((resolve) { uni.getBLEDeviceRSSI({ deviceId, success: (res) { resolve(res.RSSI) }, fail: () { resolve(-100) // 默认值 } }) }) } // 带退避的重试逻辑 async connectWithRetry(deviceId, maxRetry 3) { let retry 0 while (retry maxRetry) { try { await this.createBLEConnection(deviceId) return true } catch (err) { retry const delay Math.min(1000 * Math.pow(2, retry), 10000) await new Promise(resolve setTimeout(resolve, delay)) } } return false }7.2 数据通信优化大数据传输时要注意分包发送每包20字节添加序列号便于重组实现简单的流控机制// 分包发送大文件 async sendLargeData(data) { const chunkSize 20 const chunks [] for (let i 0; i data.length; i chunkSize) { chunks.push(data.slice(i, i chunkSize)) } for (let i 0; i chunks.length; i) { const chunk chunks[i] const header new ArrayBuffer(3) const view new DataView(header) view.setUint8(0, 0x01) // 数据类型 view.setUint16(1, i, true) // 包序号 const packet new Uint8Array(header.byteLength chunk.byteLength) packet.set(new Uint8Array(header), 0) packet.set(new Uint8Array(chunk), header.byteLength) await this.writeCharacteristic(packet.buffer) } }7.3 调试技巧调试BLE应用时这些工具很有用nRF Connect查看设备服务和特征值Wireshark抓包分析BLE通信手机开发者选项中的蓝牙HCI日志在代码中添加详细的日志// 包装原生API添加日志 const originalWrite uni.writeBLECharacteristicValue uni.writeBLECharacteristicValue function(params) { console.log(写入数据:, params.value ? this.ab2hex(params.value) : null) return originalWrite.call(this, params) }8. 跨平台兼容性处理8.1 iOS与Android差异在多个项目中我总结了这些主要差异权限处理iOS更严格需要主动请求服务发现Android可能需要更长时间MTU大小iOS通常更大185字节 vs Android的23字节// 获取MTU大小 getMtuSize(deviceId) { return new Promise((resolve) { // iOS不支持返回默认值 if (uni.getSystemInfoSync().platform ios) { resolve(185) return } uni.getBLEMTU({ deviceId, success: (res) { resolve(res.mtu) }, fail: () { resolve(23) // Android默认值 } }) }) }8.2 厂商定制ROM问题某些手机厂商的定制ROM修改了蓝牙协议栈导致华为/荣耀后台扫描需要特殊权限小米可能需要关闭MIUI优化OPPO/Vivo限制后台连接解决方法引导用户设置允许后台运行添加厂商检测逻辑// 检测手机厂商 const brand uni.getSystemInfoSync().brand.toLowerCase() if (brand.includes(huawei) || brand.includes(honor)) { // 华为系特殊处理 this.isHuawei true }8.3 微信小程序特殊处理在微信小程序环境中还需要注意必须使用wx对象而非uni需要配置合法域名后台运行限制更严格// 环境检测 const isWechat typeof wx ! undefined typeof uni undefined if (isWechat) { // 微信小程序特殊逻辑 this.ble wx } else { this.ble uni }9. 安全与隐私考量9.1 数据传输安全BLE通信本身不加密敏感数据应该使用加密特征值实现应用层加密添加消息认证码(MAC)// 简单的AES加密示例 import CryptoJS from crypto-js encryptData(data, key) { const encrypted CryptoJS.AES.encrypt( JSON.stringify(data), key ).toString() return this.str2ab(encrypted) } decryptData(buffer, key) { const str this.ab2str(buffer) const bytes CryptoJS.AES.decrypt(str, key) return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)) }9.2 用户隐私保护遵循这些最佳实践仅请求必要的权限明确告知数据用途提供隐私设置选项在隐私政策中明确说明蓝牙数据的使用方式并提供关闭数据收集的选项。10. 项目架构建议10.1 封装BLE模块将蓝牙操作封装成独立模块提高复用性// ble.js export default class BLEService { constructor() { this.deviceId null this.services {} } async connect(deviceId) { // 连接逻辑 } async read(serviceUUID, charUUID) { // 读取逻辑 } async write(serviceUUID, charUUID, data) { // 写入逻辑 } // 其他方法... } // 在页面中使用 import BLEService from ./ble.js const ble new BLEService()10.2 状态管理使用Vuex或Pinia管理蓝牙状态// store/bluetooth.js export const useBluetoothStore defineStore(bluetooth, { state: () ({ devices: [], connectedDevice: null, services: [], characteristics: [] }), actions: { async connect(deviceId) { // 连接逻辑 } } })10.3 错误统一处理实现全局错误拦截// 拦截uni API错误 const originalRequest uni.request uni.request function(options) { return originalRequest.call(this, options).catch(err { this.handleError(err) throw err }) } // 统一错误处理 handleError(err) { if (err.errCode) { // BLE错误 this.showBLEError(err.errCode) } else { // 其他错误 console.error(未知错误:, err) } }11. 测试与质量保证11.1 单元测试策略对核心功能编写测试用例// ble.test.js describe(BLE Service, () { let bleService beforeEach(() { bleService new BLEService() }) test(should convert hex to array buffer, () { const hex 0123456789abcdef const buffer bleService.hex2ab(hex) expect(bleService.ab2hex(buffer)).toBe(hex) }) })11.2 真机测试要点真机测试时重点关注不同手机型号的兼容性弱网环境下的稳定性长时间运行的稳定性建议测试矩阵手机品牌华为、小米、OPPO、vivo、三星、iPhone系统版本Android 8、iOS 12蓝牙版本4.0、4.2、5.011.3 自动化测试方案实现基本的自动化测试// 模拟BLE设备 class MockBLEDevice { constructor() { this.services [ { uuid: 0000180f-0000-1000-8000-00805f9b34fb, characteristics: [ { uuid: 00002a19-0000-1000-8000-00805f9b34fb, properties: { read: true } } ] } ] } } // 在测试中使用模拟设备 test(should read battery level, async () { const mockDevice new MockBLEDevice() const level await bleService.readBattery(mockDevice) expect(level).toBeGreaterThanOrEqual(0) expect(level).toBeLessThanOrEqual(100) })12. 进阶功能实现12.1 多设备同时连接某些场景需要同时连接多个设备实现要点维护多个deviceId为每个连接建立独立上下文实现消息路由class MultiBLE { constructor() { this.connections new Map() } async addConnection(deviceId) { const context { deviceId, services: {}, callbacks: new Set() } this.connections.set(deviceId, context) await this.setupConnection(context) return context } async send(deviceId, serviceId, charId, data) { const context this.connections.get(deviceId) if (!context) throw new Error(Device not connected) await uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: charId, value: data }) } }12.2 大数据分片传输传输大文件或固件时需要分片处理async sendFirmware(deviceId, firmware) { const chunkSize await this.getMtuSize(deviceId) - 3 // 预留头信息 const totalChunks Math.ceil(firmware.length / chunkSize) for (let i 0; i totalChunks; i) { const chunk firmware.slice(i * chunkSize, (i 1) * chunkSize) const header new ArrayBuffer(3) const view new DataView(header) view.setUint8(0, 0x02) // 固件类型 view.setUint16(1, i, true) // 分片序号 const packet new Uint8Array(header.byteLength chunk.byteLength) packet.set(new Uint8Array(header), 0) packet.set(new Uint8Array(chunk), header.byteLength) await this.write(deviceId, FIRMWARE_SERVICE, FIRMWARE_CHAR, packet.buffer) // 进度回调 this.onProgress(Math.round((i 1) / totalChunks * 100)) } }12.3 后台运行策略Android上保持后台连接的技巧使用前台服务申请WAKE_LOCK优化心跳间隔// 在AndroidManifest.xml中添加 service android:name.BluetoothService android:foregroundServiceTypeconnectedDevice /13. 性能监控与优化13.1 关键指标监控建议监控这些指标连接建立时间数据传输速率错误发生率class PerformanceMonitor { constructor() { this.metrics { connectTime: 0, transferRate: 0, errorCount: 0 } this.timers {} } start(name) { this.timers[name] Date.now() } end(name) { const duration Date.now() - this.timers[name] this.metrics[name] duration return duration } recordError() { this.metrics.errorCount } }13.2 内存优化BLE开发常见内存问题回调函数堆积未释放ArrayBuffer监听器泄漏优化建议// 使用弱引用避免内存泄漏 const callbacks new WeakMap() function addCallback(obj, fn) { if (!callbacks.has(obj)) { callbacks.set(obj, new Set()) } callbacks.get(obj).add(fn) } function removeCallback(obj, fn) { if (callbacks.has(obj)) { callbacks.get(obj).delete(fn) } }13.3 功耗优化降低功耗的技巧减少不必要的扫描优化数据传输频率使用适当的连接参数// 调整连接参数Android uni.setBLEConnectionParameters({ deviceId, interval: 80, // 1.25ms单位 latency: 0, timeout: 500 // 10ms单位 })14. 升级与维护策略14.1 固件升级(OTA)实现安全的固件升级流程校验固件签名分块传输验证CRCasync performOTA(deviceId, firmware) { // 1. 进入OTA模式 await this.write(deviceId, OTA_SERVICE, OTA_CONTROL, [0x01]) try { // 2. 发送固件 await this.sendFirmware(deviceId, firmware) // 3. 验证签名 const isValid await this.verifySignature(deviceId) if (!isValid) throw new Error(验证失败) // 4. 重启设备 await this.write(deviceId, OTA_SERVICE, OTA_CONTROL, [0x02]) return true } catch (err) { // 恢复模式 await this.write(deviceId, OTA_SERVICE, OTA_CONTROL, [0x00]) throw err } }14.2 兼容性维护维护一个设备兼容性矩阵设备型号蓝牙版本特殊要求测试结果小米125.2需要关闭MIUI优化通过华为P505.1需要后台权限通过iPhone135.0无通过14.3 错误收集与分析实现错误上报系统window.onerror (msg, url, line, col, error) { const errorInfo { msg, url, line, col, stack: error?.stack, device: uni.getSystemInfoSync() } uni.request({ url: https://your-error-service.com/log, method: POST, data: errorInfo }) }15. 替代方案对比15.1 BLE与传统蓝牙选择依据功耗BLE胜出速率传统蓝牙更快兼容性BLE更广泛15.2 uniAPP与原生开发uniAPP优势开发效率高跨平台维护成本低原生开发优势性能更好功能更全面兼容性问题少15.3 其他跨平台方案比较React Native、Flutter等方案的BLE支持情况uniAPP在中文文档和社区支持上有优势。16. 常见问题解答16.1 搜索不到设备排查步骤确认蓝牙已开启检查位置权限确认设备处于可发现模式尝试重启蓝牙16.2 连接经常断开可能原因信号干扰距离过远设备资源不足解决方案优化环境实现自动重连调整连接参数16.3 数据传输不稳定处理方法添加重试机制实现数据校验优化分包策略17. 资源推荐17.1 学习资料《蓝牙核心规范》官方文档uniAPP官方BLE文档蓝牙SIG提供的开发指南17.2 开发工具nRF Connect调试工具Wireshark协议分析BLE Scanner设备扫描17.3 社区支持uniAPP官方论坛Stack OverflowGitHub相关开源项目18. 版本更新记录维护一个清晰的更新日志## 1.2.0 (2023-06-15) - 新增支持多设备同时连接 - 优化改进重连逻辑 - 修复Android 13兼容性问题 ## 1.1.0 (2023-03-10) - 新增OTA固件升级功能 - 优化数据传输稳定性19. 法律与合规19.1 认证要求不同地区有不同的无线电认证要求中国SRRC认证美国FCC认证欧洲CE认证19.2 隐私法规确保符合GDPR、CCPA等隐私法规的要求特别是在收集用户健康数据时。20. 未来展望BLE技术仍在发展值得关注的趋势蓝牙5.2的LE Audio蓝牙Mesh网络更低功耗的优化在实际项目中我发现随着蓝牙标准的演进uniAPP的BLE API也在持续完善。建议定期关注uniAPP的更新日志及时获取新特性。
uniAPP低功耗蓝牙实战:从设备发现到数据交互的完整指南
1. 为什么选择uniAPP开发低功耗蓝牙应用如果你正在开发智能手环、智能家居设备这类需要与手机交互的硬件产品uniAPP绝对是个不错的选择。我做过十几个蓝牙相关的项目发现uniAPP的跨平台特性确实能省不少事——一套代码可以同时跑在iOS和Android上再也不用为两个平台分别开发了。低功耗蓝牙BLE和传统蓝牙最大的区别就是省电。我实测过一个智能手环项目用BLE连接手机每天同步3次数据续航能达到7天而用传统蓝牙协议同样条件下只能用3天。BLE特别适合那些不需要持续传输大量数据的场景比如心率监测、计步器、智能门锁等。uniAPP对BLE的支持相当完善。从设备搜索到连接建立从服务发现到数据读写整个流程的API都封装得很好。不过在实际项目中我发现有些坑需要注意比如Android和iOS在蓝牙权限处理上的差异不同手机厂商对BLE协议栈的实现细节等。后面我会结合具体代码告诉你如何避开这些坑。2. 开发前的准备工作2.1 环境搭建首先确保你的开发环境已经就绪。我推荐使用HBuilderX作为开发工具它针对uniAPP做了很多优化。创建一个新的uniAPP项目后需要在manifest.json里配置蓝牙权限{ mp-weixin: { requiredBackgroundModes: [bluetooth], permission: { scope.bluetooth: { desc: 需要蓝牙权限 } } } }对于Android 6.0以上的设备还需要动态申请位置权限因为BLE扫描需要用到位置服务。这个坑我踩过——明明蓝牙权限都有了就是搜不到设备后来发现是没开位置权限。解决方法是在页面加载时检查权限// 检查位置权限 uni.authorize({ scope: scope.userLocation, success() { console.log(已授权位置权限) }, fail() { uni.showModal({ title: 提示, content: 需要位置权限才能搜索蓝牙设备, showCancel: false }) } })2.2 蓝牙适配器初始化所有蓝牙操作的前提是初始化蓝牙适配器。这里有个细节要注意在iOS上如果用户拒绝蓝牙权限或者手机蓝牙硬件不可用初始化会直接失败而在Android上系统可能会自动弹出权限请求。uni.openBluetoothAdapter({ success: (res) { console.log(蓝牙适配器初始化成功) this.startDiscovery() }, fail: (err) { console.error(初始化失败, err) if (err.errCode 10001) { uni.showToast({ title: 蓝牙适配器不可用, icon: none }) } } })初始化成功后建议立即监听蓝牙适配器状态变化。我在项目中遇到过用户中途关闭蓝牙导致功能异常的情况加上这个监听就能及时处理uni.onBluetoothAdapterStateChange((res) { console.log(蓝牙适配器状态变化: ${res.available ? 可用 : 不可用}) if (!res.available) { uni.showToast({ title: 蓝牙已关闭, icon: none }) } })3. 设备搜索与连接实战3.1 高效搜索蓝牙设备开始搜索前建议先停止之前的搜索如果有的话并清空设备列表。我遇到过设备重复显示的问题就是因为没做这个清理uni.startBluetoothDevicesDiscovery({ success: (res) { console.log(开始搜索) // 监听新发现的设备 uni.onBluetoothDeviceFound((res) { const devices res.devices // 过滤掉无效设备 const validDevices devices.filter(device device.name device.deviceId ) // 去重处理 this.deviceList [...new Set([...this.deviceList, ...validDevices])] }) }, fail: (err) { console.error(搜索失败, err) } })搜索到目标设备后建议手动停止搜索以节省电量。我一般设置15秒的自动停止setTimeout(() { uni.stopBluetoothDevicesDiscovery({ success: () { console.log(已停止搜索) } }) }, 15000)3.2 稳定连接建立技巧连接设备时最常见的错误是10003连接失败。根据我的经验这通常是因为设备已经连接但状态没更新或者设备不在可连接状态。解决方法是在连接前先检查状态createBLEConnection(deviceId) { if (this.connecting) { uni.showToast({ title: 已有设备连接, icon: none }) return } uni.showLoading({ title: 连接中... }) uni.createBLEConnection({ deviceId, success: () { uni.hideLoading() console.log(连接成功) this.getServices(deviceId) }, fail: (err) { uni.hideLoading() console.error(连接失败, err) // 自动重试逻辑 if (err.errCode 10003 this.retryCount 3) { this.retryCount setTimeout(() { this.createBLEConnection(deviceId) }, 500) } } }) }连接成功后不要立即获取服务我发现在Android上延迟1秒左右更稳定setTimeout(() { this.getBLEDeviceServices(deviceId) }, 1000)4. 服务发现与数据交互4.1 理解蓝牙服务架构BLE设备采用GATT通用属性规范数据按层级组织服务Service设备提供的功能集合特征Characteristic服务中的具体数据点描述符Descriptor特征的额外信息获取服务列表后需要找到目标服务UUID。这里有个技巧打印所有服务然后根据设备文档找到对应的服务uni.getBLEDeviceServices({ deviceId, success: (res) { console.log(所有服务:, res.services) // 假设我们需要电池服务 const batteryService res.services.find( s s.uuid.toLowerCase() 0000180f-0000-1000-8000-00805f9b34fb ) if (batteryService) { this.getCharacteristics(deviceId, batteryService.uuid) } } })4.2 特征值操作详解特征值有三种主要属性read可读取write可写入notify可订阅通知获取特征值时要检查属性是否支持你要的操作uni.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) { const charList res.characteristics charList.forEach(char { if (char.properties.read) { // 可读取的特征 this.readChar char.uuid } if (char.properties.write) { // 可写入的特征 this.writeChar char.uuid } if (char.properties.notify) { // 可订阅的特征 this.notifyChar char.uuid this.enableNotify(deviceId, serviceId, char.uuid) } }) } })启用通知是个关键步骤很多同学在这里卡住。必须在成功启用后才能收到设备推送的数据enableNotify(deviceId, serviceId, characteristicId) { uni.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId, state: true, success: () { console.log(通知启用成功) // 必须在这里设置监听 uni.onBLECharacteristicValueChange((res) { console.log(收到数据:, this.ab2hex(res.value)) }) } }) }4.3 数据读写最佳实践写入数据时要注意格式转换。BLE通信使用ArrayBuffer但业务代码通常操作的是普通数组或字符串。我封装了几个转换函数// 字符串转ArrayBuffer str2ab(str) { const buf new ArrayBuffer(str.length) const bufView new Uint8Array(buf) for (let i 0; i str.length; i) { bufView[i] str.charCodeAt(i) } return buf } // 16进制字符串转ArrayBuffer hex2ab(hex) { const typedArray new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h parseInt(h, 16))) return typedArray.buffer } // ArrayBuffer转16进制字符串 ab2hex(buffer) { return Array.prototype.map.call( new Uint8Array(buffer), bit (00 bit.toString(16)).slice(-2) ).join() }写入数据示例sendCommand(cmd) { const buffer this.hex2ab(cmd) uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.writeChar, value: buffer, success: () { console.log(写入成功) }, fail: (err) { console.error(写入失败, err) } }) }5. 连接管理与异常处理5.1 完善的连接状态管理BLE连接容易受环境影响断开需要做好状态管理。我建议维护以下状态变量connecting是否正在连接connected是否已连接deviceId当前连接设备IDserviceId主服务IDcharacteristics特征值集合监听连接状态变化uni.onBLEConnectionStateChange((res) { this.connected res.connected if (!res.connected) { console.log(连接断开) this.reconnect() // 自动重连逻辑 } })5.2 常见错误处理根据我的经验这些错误最常见10001蓝牙适配器不可用 → 检查蓝牙是否开启10003连接失败 → 检查设备是否可连接尝试重试10004找不到服务 → 确认服务UUID是否正确10005找不到特征值 → 确认特征UUID是否正确实现一个统一的错误处理器handleBLEError(err) { console.error(BLE错误:, err) const map { 10001: 蓝牙不可用请检查蓝牙设置, 10003: 连接失败请重试, 10004: 服务不存在, 10005: 特征值不存在 } uni.showToast({ title: map[err.errCode] || 未知错误: ${err.errCode}, icon: none }) // 特定错误自动恢复 if (err.errCode 10001) { setTimeout(() { uni.openBluetoothAdapter() // 尝试重新初始化 }, 1000) } }5.3 资源释放页面卸载时一定要释放资源否则可能导致下次连接失败onUnload() { if (this.deviceId) { uni.closeBLEConnection({ deviceId: this.deviceId }) } uni.stopBluetoothDevicesDiscovery() // 移除所有监听 uni.offBluetoothAdapterStateChange() uni.offBLEConnectionStateChange() uni.offBLECharacteristicValueChange() }6. 实战案例智能手环数据同步以智能手环为例演示完整的数据交互流程。假设手环提供以下服务电池服务0x180F读取电量设备信息服务0x180A读取固件版本自定义服务0xFFE0同步运动数据6.1 读取设备信息async readDeviceInfo() { // 读取电池电量 const battery await this.readCharacteristic( 0000180f-0000-1000-8000-00805f9b34fb, 00002a19-0000-1000-8000-00805f9b34fb ) console.log(当前电量:, battery.getUint8(0)) // 读取固件版本 const version await this.readCharacteristic( 0000180a-0000-1000-8000-00805f9b34fb, 00002a26-0000-1000-8000-00805f9b34fb ) console.log(固件版本:, this.ab2str(version)) } readCharacteristic(serviceId, characteristicId) { return new Promise((resolve, reject) { uni.readBLECharacteristicValue({ deviceId: this.deviceId, serviceId, characteristicId, success: (res) { resolve(res.value) }, fail: reject }) }) }6.2 同步运动数据// 启用运动数据通知 enableSportDataNotify() { uni.notifyBLECharacteristicValueChange({ deviceId: this.deviceId, serviceId: 0000ffe0-0000-1000-8000-00805f9b34fb, characteristicId: 0000ffe1-0000-1000-8000-00805f9b34fb, state: true, success: () { uni.onBLECharacteristicValueChange((res) { const data this.parseSportData(res.value) console.log(步数:, data.steps) console.log(心率:, data.heartRate) }) } }) } // 解析运动数据 parseSportData(buffer) { const view new DataView(buffer) return { steps: view.getUint16(0, true), heartRate: view.getUint8(2) } }6.3 发送控制命令// 设置时间 setDeviceTime() { const now new Date() const buffer new ArrayBuffer(6) const view new DataView(buffer) view.setUint16(0, now.getFullYear(), true) view.setUint8(2, now.getMonth() 1) view.setUint8(3, now.getDate()) view.setUint8(4, now.getHours()) view.setUint8(5, now.getMinutes()) uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: 0000ffe0-0000-1000-8000-00805f9b34fb, characteristicId: 0000ffe2-0000-1000-8000-00805f9b34fb, value: buffer }) }7. 性能优化与调试技巧7.1 连接优化策略在弱信号环境下我发现这些策略很有效增加连接超时Android默认是30秒实现指数退避重试缓存设备信号强度RSSI优先连接信号强的设备// 获取设备信号强度 getRssi(deviceId) { return new Promise((resolve) { uni.getBLEDeviceRSSI({ deviceId, success: (res) { resolve(res.RSSI) }, fail: () { resolve(-100) // 默认值 } }) }) } // 带退避的重试逻辑 async connectWithRetry(deviceId, maxRetry 3) { let retry 0 while (retry maxRetry) { try { await this.createBLEConnection(deviceId) return true } catch (err) { retry const delay Math.min(1000 * Math.pow(2, retry), 10000) await new Promise(resolve setTimeout(resolve, delay)) } } return false }7.2 数据通信优化大数据传输时要注意分包发送每包20字节添加序列号便于重组实现简单的流控机制// 分包发送大文件 async sendLargeData(data) { const chunkSize 20 const chunks [] for (let i 0; i data.length; i chunkSize) { chunks.push(data.slice(i, i chunkSize)) } for (let i 0; i chunks.length; i) { const chunk chunks[i] const header new ArrayBuffer(3) const view new DataView(header) view.setUint8(0, 0x01) // 数据类型 view.setUint16(1, i, true) // 包序号 const packet new Uint8Array(header.byteLength chunk.byteLength) packet.set(new Uint8Array(header), 0) packet.set(new Uint8Array(chunk), header.byteLength) await this.writeCharacteristic(packet.buffer) } }7.3 调试技巧调试BLE应用时这些工具很有用nRF Connect查看设备服务和特征值Wireshark抓包分析BLE通信手机开发者选项中的蓝牙HCI日志在代码中添加详细的日志// 包装原生API添加日志 const originalWrite uni.writeBLECharacteristicValue uni.writeBLECharacteristicValue function(params) { console.log(写入数据:, params.value ? this.ab2hex(params.value) : null) return originalWrite.call(this, params) }8. 跨平台兼容性处理8.1 iOS与Android差异在多个项目中我总结了这些主要差异权限处理iOS更严格需要主动请求服务发现Android可能需要更长时间MTU大小iOS通常更大185字节 vs Android的23字节// 获取MTU大小 getMtuSize(deviceId) { return new Promise((resolve) { // iOS不支持返回默认值 if (uni.getSystemInfoSync().platform ios) { resolve(185) return } uni.getBLEMTU({ deviceId, success: (res) { resolve(res.mtu) }, fail: () { resolve(23) // Android默认值 } }) }) }8.2 厂商定制ROM问题某些手机厂商的定制ROM修改了蓝牙协议栈导致华为/荣耀后台扫描需要特殊权限小米可能需要关闭MIUI优化OPPO/Vivo限制后台连接解决方法引导用户设置允许后台运行添加厂商检测逻辑// 检测手机厂商 const brand uni.getSystemInfoSync().brand.toLowerCase() if (brand.includes(huawei) || brand.includes(honor)) { // 华为系特殊处理 this.isHuawei true }8.3 微信小程序特殊处理在微信小程序环境中还需要注意必须使用wx对象而非uni需要配置合法域名后台运行限制更严格// 环境检测 const isWechat typeof wx ! undefined typeof uni undefined if (isWechat) { // 微信小程序特殊逻辑 this.ble wx } else { this.ble uni }9. 安全与隐私考量9.1 数据传输安全BLE通信本身不加密敏感数据应该使用加密特征值实现应用层加密添加消息认证码(MAC)// 简单的AES加密示例 import CryptoJS from crypto-js encryptData(data, key) { const encrypted CryptoJS.AES.encrypt( JSON.stringify(data), key ).toString() return this.str2ab(encrypted) } decryptData(buffer, key) { const str this.ab2str(buffer) const bytes CryptoJS.AES.decrypt(str, key) return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)) }9.2 用户隐私保护遵循这些最佳实践仅请求必要的权限明确告知数据用途提供隐私设置选项在隐私政策中明确说明蓝牙数据的使用方式并提供关闭数据收集的选项。10. 项目架构建议10.1 封装BLE模块将蓝牙操作封装成独立模块提高复用性// ble.js export default class BLEService { constructor() { this.deviceId null this.services {} } async connect(deviceId) { // 连接逻辑 } async read(serviceUUID, charUUID) { // 读取逻辑 } async write(serviceUUID, charUUID, data) { // 写入逻辑 } // 其他方法... } // 在页面中使用 import BLEService from ./ble.js const ble new BLEService()10.2 状态管理使用Vuex或Pinia管理蓝牙状态// store/bluetooth.js export const useBluetoothStore defineStore(bluetooth, { state: () ({ devices: [], connectedDevice: null, services: [], characteristics: [] }), actions: { async connect(deviceId) { // 连接逻辑 } } })10.3 错误统一处理实现全局错误拦截// 拦截uni API错误 const originalRequest uni.request uni.request function(options) { return originalRequest.call(this, options).catch(err { this.handleError(err) throw err }) } // 统一错误处理 handleError(err) { if (err.errCode) { // BLE错误 this.showBLEError(err.errCode) } else { // 其他错误 console.error(未知错误:, err) } }11. 测试与质量保证11.1 单元测试策略对核心功能编写测试用例// ble.test.js describe(BLE Service, () { let bleService beforeEach(() { bleService new BLEService() }) test(should convert hex to array buffer, () { const hex 0123456789abcdef const buffer bleService.hex2ab(hex) expect(bleService.ab2hex(buffer)).toBe(hex) }) })11.2 真机测试要点真机测试时重点关注不同手机型号的兼容性弱网环境下的稳定性长时间运行的稳定性建议测试矩阵手机品牌华为、小米、OPPO、vivo、三星、iPhone系统版本Android 8、iOS 12蓝牙版本4.0、4.2、5.011.3 自动化测试方案实现基本的自动化测试// 模拟BLE设备 class MockBLEDevice { constructor() { this.services [ { uuid: 0000180f-0000-1000-8000-00805f9b34fb, characteristics: [ { uuid: 00002a19-0000-1000-8000-00805f9b34fb, properties: { read: true } } ] } ] } } // 在测试中使用模拟设备 test(should read battery level, async () { const mockDevice new MockBLEDevice() const level await bleService.readBattery(mockDevice) expect(level).toBeGreaterThanOrEqual(0) expect(level).toBeLessThanOrEqual(100) })12. 进阶功能实现12.1 多设备同时连接某些场景需要同时连接多个设备实现要点维护多个deviceId为每个连接建立独立上下文实现消息路由class MultiBLE { constructor() { this.connections new Map() } async addConnection(deviceId) { const context { deviceId, services: {}, callbacks: new Set() } this.connections.set(deviceId, context) await this.setupConnection(context) return context } async send(deviceId, serviceId, charId, data) { const context this.connections.get(deviceId) if (!context) throw new Error(Device not connected) await uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: charId, value: data }) } }12.2 大数据分片传输传输大文件或固件时需要分片处理async sendFirmware(deviceId, firmware) { const chunkSize await this.getMtuSize(deviceId) - 3 // 预留头信息 const totalChunks Math.ceil(firmware.length / chunkSize) for (let i 0; i totalChunks; i) { const chunk firmware.slice(i * chunkSize, (i 1) * chunkSize) const header new ArrayBuffer(3) const view new DataView(header) view.setUint8(0, 0x02) // 固件类型 view.setUint16(1, i, true) // 分片序号 const packet new Uint8Array(header.byteLength chunk.byteLength) packet.set(new Uint8Array(header), 0) packet.set(new Uint8Array(chunk), header.byteLength) await this.write(deviceId, FIRMWARE_SERVICE, FIRMWARE_CHAR, packet.buffer) // 进度回调 this.onProgress(Math.round((i 1) / totalChunks * 100)) } }12.3 后台运行策略Android上保持后台连接的技巧使用前台服务申请WAKE_LOCK优化心跳间隔// 在AndroidManifest.xml中添加 service android:name.BluetoothService android:foregroundServiceTypeconnectedDevice /13. 性能监控与优化13.1 关键指标监控建议监控这些指标连接建立时间数据传输速率错误发生率class PerformanceMonitor { constructor() { this.metrics { connectTime: 0, transferRate: 0, errorCount: 0 } this.timers {} } start(name) { this.timers[name] Date.now() } end(name) { const duration Date.now() - this.timers[name] this.metrics[name] duration return duration } recordError() { this.metrics.errorCount } }13.2 内存优化BLE开发常见内存问题回调函数堆积未释放ArrayBuffer监听器泄漏优化建议// 使用弱引用避免内存泄漏 const callbacks new WeakMap() function addCallback(obj, fn) { if (!callbacks.has(obj)) { callbacks.set(obj, new Set()) } callbacks.get(obj).add(fn) } function removeCallback(obj, fn) { if (callbacks.has(obj)) { callbacks.get(obj).delete(fn) } }13.3 功耗优化降低功耗的技巧减少不必要的扫描优化数据传输频率使用适当的连接参数// 调整连接参数Android uni.setBLEConnectionParameters({ deviceId, interval: 80, // 1.25ms单位 latency: 0, timeout: 500 // 10ms单位 })14. 升级与维护策略14.1 固件升级(OTA)实现安全的固件升级流程校验固件签名分块传输验证CRCasync performOTA(deviceId, firmware) { // 1. 进入OTA模式 await this.write(deviceId, OTA_SERVICE, OTA_CONTROL, [0x01]) try { // 2. 发送固件 await this.sendFirmware(deviceId, firmware) // 3. 验证签名 const isValid await this.verifySignature(deviceId) if (!isValid) throw new Error(验证失败) // 4. 重启设备 await this.write(deviceId, OTA_SERVICE, OTA_CONTROL, [0x02]) return true } catch (err) { // 恢复模式 await this.write(deviceId, OTA_SERVICE, OTA_CONTROL, [0x00]) throw err } }14.2 兼容性维护维护一个设备兼容性矩阵设备型号蓝牙版本特殊要求测试结果小米125.2需要关闭MIUI优化通过华为P505.1需要后台权限通过iPhone135.0无通过14.3 错误收集与分析实现错误上报系统window.onerror (msg, url, line, col, error) { const errorInfo { msg, url, line, col, stack: error?.stack, device: uni.getSystemInfoSync() } uni.request({ url: https://your-error-service.com/log, method: POST, data: errorInfo }) }15. 替代方案对比15.1 BLE与传统蓝牙选择依据功耗BLE胜出速率传统蓝牙更快兼容性BLE更广泛15.2 uniAPP与原生开发uniAPP优势开发效率高跨平台维护成本低原生开发优势性能更好功能更全面兼容性问题少15.3 其他跨平台方案比较React Native、Flutter等方案的BLE支持情况uniAPP在中文文档和社区支持上有优势。16. 常见问题解答16.1 搜索不到设备排查步骤确认蓝牙已开启检查位置权限确认设备处于可发现模式尝试重启蓝牙16.2 连接经常断开可能原因信号干扰距离过远设备资源不足解决方案优化环境实现自动重连调整连接参数16.3 数据传输不稳定处理方法添加重试机制实现数据校验优化分包策略17. 资源推荐17.1 学习资料《蓝牙核心规范》官方文档uniAPP官方BLE文档蓝牙SIG提供的开发指南17.2 开发工具nRF Connect调试工具Wireshark协议分析BLE Scanner设备扫描17.3 社区支持uniAPP官方论坛Stack OverflowGitHub相关开源项目18. 版本更新记录维护一个清晰的更新日志## 1.2.0 (2023-06-15) - 新增支持多设备同时连接 - 优化改进重连逻辑 - 修复Android 13兼容性问题 ## 1.1.0 (2023-03-10) - 新增OTA固件升级功能 - 优化数据传输稳定性19. 法律与合规19.1 认证要求不同地区有不同的无线电认证要求中国SRRC认证美国FCC认证欧洲CE认证19.2 隐私法规确保符合GDPR、CCPA等隐私法规的要求特别是在收集用户健康数据时。20. 未来展望BLE技术仍在发展值得关注的趋势蓝牙5.2的LE Audio蓝牙Mesh网络更低功耗的优化在实际项目中我发现随着蓝牙标准的演进uniAPP的BLE API也在持续完善。建议定期关注uniAPP的更新日志及时获取新特性。