1. uni-app蓝牙开发中的监听异常问题
第一次接触uni-app蓝牙开发时,最让我头疼的就是设备监听重复的问题。明明只扫描了一次,设备列表里却出现了好几个相同的设备。后来才发现,这是因为没有正确管理监听事件导致的。
uni-app的蓝牙API设计得比较基础,不像原生小程序那样提供了offBluetoothDeviceFound这样的取消监听方法。这就意味着每次调用onBluetoothDeviceFound都会新增一个监听器,如果不做处理,自然就会出现重复设备的问题。
我当时的解决方案是建立一个全局事件总线。具体做法是在utils文件夹下创建一个constants.js文件,定义事件名称常量:
// utils/constants.js export const BLUETOOTH_DEVICE_LIST = "deviceList";然后在另一个工具文件中封装蓝牙监听逻辑:
// utils/listenBluetoothAPI.js import { BLUETOOTH_DEVICE_LIST } from '@/utils/constants.js' export function onBluetoothDeviceFound() { uni.onBluetoothDeviceFound(res => { getBluetoothDevices() }) } function getBluetoothDevices() { uni.getBluetoothDevices({ success(res) { uni.$emit(BLUETOOTH_DEVICE_LIST, res.devices) } }) }这样做的妙处在于,我们通过uni.$emit和uni.$on建立了一个事件中心,所有页面都可以订阅这个事件,但又不会产生多个监听器。在App.vue中初始化监听:
import { onBluetoothDeviceFound } from '@/utils/listenBluetoothAPI.js' export default { onLaunch() { onBluetoothDeviceFound() } }在具体页面中使用时,记得在onUnload生命周期中取消监听,避免内存泄漏:
uni.$on(BLUETOOTH_DEVICE_LIST, function(res) { // 处理设备列表 }) onUnload(() => { uni.$off(BLUETOOTH_DEVICE_LIST) })2. 蓝牙特征值读写中的事件累加问题
蓝牙通信中另一个常见问题是特征值变化事件的累加。这个问题更加隐蔽,往往在多次读写操作后才会显现出来。
我遇到过这样一个案例:向设备写入数据后,监听特征值变化的回调函数会被多次触发。最初以为是设备的问题,后来发现是uni.onBLECharacteristicValueChange监听器没有正确移除导致的。
解决这个问题的关键在于引入节流机制。我的做法是记录上次调用的时间戳,如果两次调用间隔过短就直接返回:
let lastCalled = 0 function checkCalled() { const current = Date.now() if (current - lastCalled < 500) { return true } lastCalled = current return false } uni.onBLECharacteristicValueChange(event => { if (checkCalled()) return // 正常处理逻辑 })对于需要精确控制的情况,可以采用计数器方案:
let successCount = 0 let retryCount = 0 const readResults = [] uni.onBLECharacteristicValueChange(event => { if (event.characteristicId !== targetId) return successCount++ readResults.push(ab2hex(event.value)) const intervalId = setInterval(() => { retryCount++ }, 1000) if (readResults.length > 0) { if (successCount >= 4) { clearInterval(intervalId) processResults(readResults) resetCounters() } } else if (retryCount > 20) { clearInterval(intervalId) resetCounters() } }) function resetCounters() { successCount = 0 retryCount = 0 readResults.length = 0 }3. 蓝牙数据流的稳定处理方案
蓝牙通信中最复杂的部分莫过于数据流处理了。由于蓝牙传输的特性,大数据往往会被拆分成多个包发送,这就需要在接收端进行重组。
我总结了一套比较稳定的处理方案。首先定义数据包的头部格式,通常包含以下信息:
- 数据包序号
- 总包数
- 当前包数据长度
- 校验和
接收端的处理流程如下:
const packets = {} let expectedSeq = 0 function processDataPacket(deviceId, packet) { // 校验数据包合法性 if (!validatePacket(packet)) { requestResend(deviceId, expectedSeq) return } // 存储数据包 packets[packet.seq] = packet.data // 检查是否收齐所有包 if (Object.keys(packets).length === packet.total) { const fullData = reassemblePackets(packets, packet.total) processCompleteData(fullData) resetPacketState() expectedSeq = 0 } else { expectedSeq++ } }为了提高稳定性,还需要实现超时重传机制:
const timeoutMap = new Map() function waitForPacket(deviceId, seq, timeout = 3000) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { timeoutMap.delete(deviceId) reject(new Error('Timeout waiting for packet')) }, timeout) timeoutMap.set(deviceId, { seq, timer, resolve }) }) } // 收到数据包时检查是否有对应的等待 function onPacketReceived(deviceId, packet) { const waiting = timeoutMap.get(deviceId) if (waiting && waiting.seq === packet.seq) { clearTimeout(waiting.timer) waiting.resolve(packet) timeoutMap.delete(deviceId) } }4. 跨平台兼容性处理技巧
uni-app的优势在于跨平台,但这也带来了兼容性问题。不同平台在蓝牙实现上存在差异,需要特别注意。
首先是在iOS和Android上的扫描差异。Android可以持续扫描,而iOS扫描一段时间后会自动停止。我的处理方式是:
let scanTimer = null function startScan() { uni.startBluetoothDevicesDiscovery({ success: () => { // Android每5秒重启一次扫描 if (uni.getSystemInfoSync().platform === 'android') { scanTimer = setInterval(() => { uni.stopBluetoothDevicesDiscovery({ success: () => { uni.startBluetoothDevicesDiscovery() } }) }, 5000) } } }) } function stopScan() { if (scanTimer) clearInterval(scanTimer) uni.stopBluetoothDevicesDiscovery() }其次是连接超时设置。iOS的连接超时比Android短很多,需要统一处理:
function connectDevice(deviceId) { return new Promise((resolve, reject) => { uni.createBLEConnection({ deviceId, timeout: 10000, // 统一设置为10秒 success: resolve, fail: reject }) // iOS额外处理 if (uni.getSystemInfoSync().platform === 'ios') { setTimeout(() => { uni.closeBLEConnection({ deviceId }) reject(new Error('Connection timeout')) }, 10000) } }) }最后是MTU(最大传输单元)的问题。Android可以协商更大的MTU,而iOS有固定限制:
function setupMTU(deviceId) { return new Promise((resolve) => { if (uni.getSystemInfoSync().platform === 'android') { uni.setBLEMTU({ deviceId, mtu: 512, success: (res) => { resolve(res.mtu) }, fail: () => { resolve(23) // 使用默认值 } }) } else { resolve(23) // iOS使用默认值 } }) }5. 蓝牙连接状态管理实践
稳定的蓝牙应用离不开良好的连接状态管理。我设计了一个状态机来管理蓝牙连接的全生命周期。
首先定义状态常量:
const BTState = { DISCONNECTED: 0, CONNECTING: 1, CONNECTED: 2, DISCONNECTING: 3, ERROR: 4 }然后创建状态管理类:
class BluetoothManager { constructor() { this.state = BTState.DISCONNECTED this.deviceId = null this.reconnectAttempts = 0 this.maxReconnectAttempts = 3 } async connect(deviceId) { if (this.state !== BTState.DISCONNECTED) return this.state = BTState.CONNECTING this.deviceId = deviceId try { await connectDevice(deviceId) this.state = BTState.CONNECTED this.reconnectAttempts = 0 this.setupListeners() } catch (err) { this.state = BTState.ERROR this.handleError(err) } } setupListeners() { uni.onBLEConnectionStateChange(res => { if (!res.connected) { this.state = BTState.DISCONNECTED this.tryReconnect() } }) } tryReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++ setTimeout(() => this.connect(this.deviceId), 1000) } } async disconnect() { if (this.state !== BTState.CONNECTED) return this.state = BTState.DISCONNECTING try { await uni.closeBLEConnection({ deviceId: this.deviceId }) this.state = BTState.DISCONNECTED } catch (err) { this.state = BTState.ERROR } } }6. 性能优化与内存管理
蓝牙应用如果不注意性能优化,很容易出现内存泄漏和卡顿问题。以下是我总结的几个关键点:
首先是设备列表的渲染优化。蓝牙扫描会频繁更新设备列表,直接绑定到视图会导致界面卡顿:
// 不好的做法 this.devices = scannedDevices // 优化做法 const renderDevices = throttle(() => { this.$set(this, 'devices', [...scannedDevices]) }, 500) uni.$on(BLUETOOTH_DEVICE_LIST, renderDevices)其次是特征值读取的批处理。频繁读取会降低性能,应该合并请求:
async function readMultipleCharacteristics(deviceId, serviceId, charIds) { const results = {} const batchSize = 3 // 每次批量读取3个特征值 const delay = 100 // 每个批次间隔100ms for (let i = 0; i < charIds.length; i += batchSize) { const batch = charIds.slice(i, i + batchSize) await Promise.all(batch.map(charId => { return readCharacteristic(deviceId, serviceId, charId) .then(value => { results[charId] = value }) })) await new Promise(resolve => setTimeout(resolve, delay)) } return results }最后是监听器的清理。务必在页面卸载时移除所有监听器:
onUnload() { uni.$off(BLUETOOTH_DEVICE_LIST) uni.offBLECharacteristicValueChange(this.onCharChange) uni.offBLEConnectionStateChange(this.onConnStateChange) }7. 调试技巧与问题排查
蓝牙开发中遇到问题时,系统的调试方法能节省大量时间。以下是我的调试工具箱:
首先是日志记录。我通常会建立一个蓝牙专用的日志系统:
const bluetoothLog = [] function logBluetoothEvent(type, data) { const entry = { timestamp: Date.now(), type, data: JSON.parse(JSON.stringify(data)) // 深拷贝 } bluetoothLog.push(entry) if (bluetoothLog.length > 100) { bluetoothLog.shift() } console.log(`[BT ${type}]`, data) } // 在所有蓝牙回调中添加日志 uni.onBluetoothDeviceFound(res => { logBluetoothEvent('deviceFound', res) })其次是状态可视化。在开发环境中,我会添加一个调试面板:
<template> <div v-if="isDev" class="debug-panel"> <h3>蓝牙调试信息</h3> <div>连接状态: {{ connectionState }}</div> <div>最近错误: {{ lastError }}</div> <button @click="exportLogs">导出日志</button> </div> </template> <script> export default { data() { return { isDev: process.env.NODE_ENV === 'development', connectionState: 'disconnected', lastError: null } }, methods: { exportLogs() { const blob = new Blob([JSON.stringify(bluetoothLog)], {type: 'application/json'}) const url = URL.createObjectURL(blob) // 下载逻辑... } } } </script>最后是使用蓝牙调试工具。推荐以下工具组合:
- nRF Connect:功能全面的蓝牙调试APP
- Wireshark:配合蓝牙适配器可以抓取蓝牙数据包
- 手机开发者模式中的蓝牙HCI日志