uni-app跨端低功耗蓝牙实战:搜索连接、GATT收发数据全流程
2026/9/13 13:00:39 网站建设 项目流程

今年上半年我在做一款运动健康类App,核心功能之一就是连接蓝牙体脂秤,读取体重、体脂、心率等数据。技术栈选了uni-app,要求一套代码同时跑Android和iOS。低功耗蓝牙这部分做到一半就发现,uni-app官方文档的BLE章节其实不算少,但App端和微信小程序端的行为差别非常大,文档里根本没有展开讲;网上能搜到的完整示例,又绝大多数是H5或小程序场景,真正能在安卓手机上稳定跑通的App端蓝牙代码非常少。

这篇文章把我这段时间踩过的坑和最终沉淀下来的一套方案整理出来,核心是BLE的搜索、连接、服务发现、数据收发,全部用uni-app实现。如果你接下来要做智能硬件类的App、运动健康类工具,或者只是想搞明白uni-app的BLE API到底怎么串起来,这篇应该能帮你少走不少弯路。

1. 先从需求说起:我要用uni-app连接哪类BLE设备

1.1 项目背景

我做的这个App主要面向家庭健康场景,需要连接体脂秤、心率带、血压计这类BLE设备。用户的手机型号五花八门,iOS和Android各占一半,所以摆在面前的第一道题就是跨端方案选型。由于产品规划里后续还要出微信小程序版本,直接用uni-app可以一套代码同时覆盖App和小程序,业务侧不用维护两套逻辑。

项目的业务逻辑其实不复杂:打开页面后自动搜索附近设备,用户点选某个设备发起连接,连接成功后通过GATT协议读取设备上报的数据,并把数据解析成人体指标展示出来。真正的复杂度在于蓝牙状态机的管理,搜索、连接、发现服务、订阅特征值、收发数据,每一步都有异步回调,如果不在工程层面做统一封装,页面代码会被回调嵌套塞满,后面接手的人肯定骂娘。

1.2 为什么选uni-app而不是原生或Flutter

做这个选型之前我认真对比过三种方案。

原生开发的问题很现实:Android和iOS各写一套蓝牙代码,工作量大一倍,而且两端的蓝牙行为差异特别多,需要专门的人分别踩坑。我们的团队不大,不想把精力耗在双端维护上。

Flutter我也考察过,生态里的BLE插件确实有几个,但坑更多。最主要的问题是iOS端稳定性参差不齐,不同的插件对CoreBluetooth的封装深度不一样,有的插件连后台恢复连接都没做,有的插件在设备断连后回调会丢失。搜热词就能看到不少人问“flutter 低功耗蓝牙ios有问题嘛”,这不是个别现象。uni-app这边反而是官方直接把原生层的蓝牙能力封装成了统一API,HBuilderX云打包也方便,最终就定了uni-app。

uni-app的BLE API设计思路和小程序端非常接近,核心是一组uni.xxxBluetoothXxx方法。如果你之前写过微信小程序蓝牙,迁移过来会非常快。但要注意:App端的API虽然同名,底层实现和权限模型跟小程序完全不同,这也是很多人照着小程序Demo写,结果在App端跑不通的根本原因。

1.3 BLE基础概念扫盲

低功耗蓝牙(BLE)和经典蓝牙最大的区别就是“按需通信”,平时不传数据时处于休眠状态,功耗极低。所以BLE不适合传大文件,适合传小数据包,比如传感器指标、遥控指令这一类。

要理解BLE的通信模型,先记住三个角色:

  • 中心设备(Central):发起扫描和连接的一方,比如手机App。
  • 外围设备(Peripheral):广播自己并等待连接的一方,比如体脂秤。
  • GATT服务(Server/Client):连接建立后,外围设备作为GATT Server,中心设备作为GATT Client,通过服务、特征值、描述符三级结构交换数据。

很多人第一次接触GATT会懵,其实可以这样理解:一个BLE设备就像一栋楼,楼里有很多房间(服务),每个房间里有很多开关和仪表(特征值),你进了楼以后要找到对应的房间,再操作对应的开关。服务和特征值都用一个128位UUID标识,有些标准服务有16位的短UUID,比如电池服务是0x180F,设备信息服务是0x180A

特征值支持的操作由properties字段决定,常见的有read、write、notify、indicate。read就是主动去读一次,write就是从手机往设备写数据,notify和indicate都是设备主动往手机推数据,区别在于indicate有应答确认而notify没有。在实际开发里,传感器数据基本都是通过notify或indicate上来的,因为设备产生数据的时间是随机的,不可能让App一直轮询。

2. 蓝牙搜索与连接的完整流程拆解

2.1 BLE通信标准流程

BLE从开始扫描到正常通信,流程是有严格顺序的,每一步操作都要在上一部的成功回调里进行,不能跳步。完整流程是这样的:

  1. 初始化蓝牙适配器(uni.openBluetoothAdapter),这一步会检查手机蓝牙是否开启。
  2. 开始扫描外围设备(uni.startBluetoothDevicesDiscovery),同时监听found事件。
  3. 搜索到目标设备后停止扫描(uni.stopBluetoothDevicesDiscovery)。
  4. 发起连接(uni.createBLEConnection)。
  5. 连接成功后获取设备的服务列表(uni.getBLEDeviceServices)。
  6. 根据业务需求,在某个服务里获取特征值列表(uni.getBLEDeviceCharacteristics)。
  7. 对需要设备主动上报数据的特征值,开启notify订阅(uni.notifyBLECharacteristicValueChange)。
  8. 通过write方法向设备下发指令,通过onBLECharacteristicValueChange监听设备上报数据。
  9. 页面销毁或业务结束时断开连接(uni.closeBLEConnection)。

这个顺序和原生CoreBluetooth、Android BLE的开发流程基本一一对应。如果你只是按某个教程写了一部分步骤,比如连接成功后直接write数据,没有先去获取服务,大概率会收到一个“service not found”之类的报错。

2.2 核心API清单与调用时机

这里我把uni-app App端BLE开发常用的API按调用阶段整理成一张表,后面写代码时可以直接对照:

阶段API作用与注意事项
初始化uni.openBluetoothAdapter打开蓝牙适配器,返回失败表示手机蓝牙未开启或权限未授予
初始化uni.getBluetoothAdapterState获取蓝牙适配器状态,可用于页面检查开关状态
搜索uni.startBluetoothDevicesDiscovery开始扫描,参数allowDuplicatesKey控制是否过滤重复设备
搜索uni.onBluetoothDeviceFound监听扫描到设备,外层devices字段是数组
搜索uni.stopBluetoothDevicesDiscovery停止扫描,连接前最好先停止,避免资源冲突
连接uni.createBLEConnection建立连接,参数deviceId就是搜索到的deviceId,可传timeout
连接uni.onBLEConnectionStateChange监听连接/断开状态变化,掉线重连要用
服务发现uni.getBLEDeviceServices获取服务UUID列表
特征值uni.getBLEDeviceCharacteristics获取指定服务下的特征值列表
数据收uni.notifyBLECharacteristicValueChange开启notify订阅,参数state设为true
数据收uni.onBLECharacteristicValueChange监听设备通过notify推上来的数据
数据发uni.writeBLECharacteristicValue向设备的特征值写入ArrayBuffer数据
断开uni.closeBLEConnection断开连接,释放资源

这个表里最容易漏的一步是notifyBLECharacteristicValueChange。很多新手以为连接成功就能直接收到设备的数据,实际不行。对于大多数用notify上报数据的传感器设备,不主动订阅特征值,设备根本不会推数据。

2.3 权限配置与Android/iOS差异

BLE开发在不同平台的权限模型差别非常大,这里重点说。

Android在6.0到11之间,蓝牙扫描需要定位权限。这个其实是个历史遗留规则,因为蓝牙扫描可以间接获取用户位置,系统安全策略就要求App必须拿到定位权限才能扫描周围设备。所以你得在manifest和运行时都申请位置权限,而且部分国产ROM(比如小米、华为)还要求用户打开系统定位服务开关,否则扫描接口直接静默失败。

Android 12及以上引入了新的蓝牙权限模型,把原来的蓝牙开关权限拆成了BLUETOOTH_SCANBLUETOOTH_CONNECT。在uni-app里配置manifest的Android权限时,最好把老的和新的权限都声明上,兼容不同版本。

iOS端主要是Info.plist里必须有NSBluetoothAlwaysUsageDescription,否则系统会在调用蓝牙接口时直接杀掉App。这个隐私描述在uni-app的manifest.json里配置,具体位置是app-plus的distribute-ios-privacyDescription。

除此之外,iOS的deviceId是系统为每个蓝牙设备生成的UUID,不是固定不变的硬件地址。同一台设备在系统蓝牙重置、App卸载重装后,这个UUID可能发生变化。所以不能把iOS的deviceId当作设备的永久业务ID来存数据库,业务上应该用设备MAC地址(如果协议广播里带)或自定义设备标识来识别。

3. 可直接复用的BluetoothManager封装(代码)

3.1 权限与manifest配置

先看manifest.json的配置。在源码视图里找到app-plus节点,往permissions和distribute里加权限:

{ "app-plus": { "permissions": { "Android": { "android.permission.BLUETOOTH": {}, "android.permission.BLUETOOTH_ADMIN": {}, "android.permission.ACCESS_FINE_LOCATION": {}, "android.permission.ACCESS_COARSE_LOCATION": {}, "android.permission.BLUETOOTH_SCAN": {}, "android.permission.BLUETOOTH_CONNECT": {} } }, "distribute": { "ios": { "privacyDescription": { "NSBluetoothAlwaysUsageDescription": "需要使用蓝牙连接智能体脂秤设备" } } } } }

注意HBuilderX不同版本的manifest配置位置可能略有差异,如果你们用的版本界面没有这些字段,切到源码视图手动加也能生效。

如果你的App还需要申请定位权限,建议做成一个统一的权限检查函数,在进入蓝牙搜索页面之前调用。比如可以检查uni.getSystemInfoSync().platform,如果是Android平台再走uni.authorize,这里代码不展开写,在实际项目里这一步要和产品流程串好。

3.2 蓝牙工具类完整封装

我一般会把蓝牙操作统一封装成一个单例BluetoothManager,避免页面里到处都是uni.xxx回调。下面这段代码是我在项目里用的核心类,去掉了业务相关部分,通用性比较强:

class BluetoothManager { constructor() { this.adapterAvailable = false; this.connected = false; this.deviceId = ''; this.serviceId = ''; this.writeUUID = ''; this.notifyUUID = ''; } /** * 初始化蓝牙适配器 */ init() { return new Promise((resolve, reject) => { uni.openBluetoothAdapter({ success: (res) => { this.adapterAvailable = true; // 初始化时需要用额外变量保存回调,避免重复注册监听 this._registerGlobalListeners(); resolve(res); }, fail: (err) => { // 常见错误:10001表示蓝牙未开启,10012表示未授权 this.adapterAvailable = false; reject(err); } }); }); } /** * 注册全局监听,只需要注册一次 */ _registerGlobalListeners() { // 监听连接状态 uni.onBLEConnectionStateChange((res) => { if (!res.connected) { this.connected = false; this.deviceId = ''; } }); // 监听设备上报数据 uni.onBLECharacteristicValueChange((res) => { if (this.onData) { this.onData(res); } }); } /** * 开始搜索设备 */ startScan(onFound) { return new Promise((resolve, reject) => { // 先停掉上一次扫描,再开始新扫描,避免状态残留 uni.stopBluetoothDevicesDiscovery({ complete: () => { uni.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, success: () => { // found事件放在start成功之后再注册,防止漏掉设备 uni.onBluetoothDeviceFound((res) => { const devices = res.devices || []; onFound && onFound(devices); }); resolve(); }, fail: (err) => reject(err) }); } }); }); } /** * 停止扫描 */ stopScan() { return new Promise((resolve) => { uni.stopBluetoothDevicesDiscovery({ complete: resolve }); }); } /** * 连接设备 */ connect(deviceId) { return new Promise((resolve, reject) => { uni.createBLEConnection({ deviceId, timeout: 10000, success: () => { this.deviceId = deviceId; this.connected = true; resolve(); }, fail: (err) => reject(err) }); }); } /** * 获取全部服务,找到业务指定的服务UUID */ findService(targetServiceUUID) { return new Promise((resolve, reject) => { uni.getBLEDeviceServices({ deviceId: this.deviceId, success: (res) => { const services = res.services || []; const target = services.find((item) => { return item.uuid.toUpperCase() === targetServiceUUID.toUpperCase(); }); if (target) { this.serviceId = target.uuid; resolve(target); } else { reject(new Error('未找到目标服务')); } }, fail: (err) => reject(err) }); }); } /** * 获取特征值,并保存写入特征值和通知特征值的UUID */ findCharacteristics(serviceId, writeUUID, notifyUUID) { return new Promise((resolve, reject) => { uni.getBLEDeviceCharacteristics({ deviceId: this.deviceId, serviceId, success: (res) => { const chars = res.characteristics || []; const writeTarget = chars.find((item) => item.uuid.toUpperCase() === writeUUID.toUpperCase()); const notifyTarget = chars.find((item) => item.uuid.toUpperCase() === notifyUUID.toUpperCase()); if (!writeTarget || !notifyTarget) { reject(new Error('未找到目标特征值')); return; } this.writeUUID = writeTarget.uuid; this.notifyUUID = notifyTarget.uuid; resolve({ writeTarget, notifyTarget }); }, fail: (err) => reject(err) }); }); } /** * 订阅notify,让设备主动上报数据 */ subscribeNotify() { return new Promise((resolve, reject) => { uni.notifyBLECharacteristicValueChange({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.notifyUUID, state: true, success: resolve, fail: reject }); }); } /** * 写入数据,value需要是ArrayBuffer */ write(value) { return new Promise((resolve, reject) => { uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.writeUUID, value, success: resolve, fail: (err) => reject(err) }); }); } /** * 断开连接 */ close() { return new Promise((resolve) => { if (this.deviceId) { uni.closeBLEConnection({ deviceId: this.deviceId, complete: () => { this.connected = false; this.deviceId = ''; resolve(); } }); } else { resolve(); } }); } } export default new BluetoothManager();

这里有几个细节值得说一下。

startScan里我刻意先调了一次stopBluetoothDevicesDiscovery,是因为实际开发中经常遇到用户反复进出搜索页的情况,如果上一次的扫描没有完全停掉,再次start可能会不回调found事件。先stop再start是一个保险做法。

findServicefindCharacteristics用了精确匹配UUID的方式,而不是靠遍历列表猜通道。因为很多设备的服务列表里除了业务服务,还有电池服务、设备信息服务这些标准服务。如果只按顺序取前几个,非常容易拿到错误的服务。最好从设备厂商的GATT表里确认业务服务UUID,然后硬匹配。

write方法的参数是ArrayBuffer,不是字符串,也不是普通数组。这个坑待会儿在问题排查里细说。

3.3 页面调用示例

下面是一个简单的uni-app页面示例,演示了搜索、连接、订阅notify、发送指令的完整流程。

<template> <view class="container"> <button @click="initAndScan">扫描设备</button> <view v-for="(item, index) in deviceList" :key="index" class="device-item" @click="connectDevice(item)"> {{ item.name || item.localName || item.deviceId }} </view> <text v-if="connectedMsg">{{ connectedMsg }}</text> </view> </template> <script> import BluetoothManager from '@/utils/bluetoothManager.js'; export default { data() { return { deviceList: [], connectedMsg: '', targetServiceUUID: '0000FFE0-0000-1000-8000-00805F9B34FB', targetWriteUUID: '0000FFE2-0000-1000-8000-00805F9B34FB', targetNotifyUUID: '0000FFE1-0000-1000-8000-00805F9B34FB' }; }, onUnload() { BluetoothManager.close(); }, methods: { initAndScan() { BluetoothManager.init().then(() => { this.deviceList = []; BluetoothManager.startScan((devices) => { devices.forEach((device) => { const key = device.deviceId; const existIndex = this.deviceList.findIndex((item) => item.deviceId === key); if (existIndex === -1) { this.deviceList.push(device); } }); }); }).catch(() => { uni.showToast({ title: '初始化蓝牙失败', icon: 'none' }); }); }, connectDevice(device) { BluetoothManager.stopScan().then(() => { return BluetoothManager.connect(device.deviceId); }).then(() => { return BluetoothManager.findService(this.targetServiceUUID); }).then(() => { return BluetoothManager.findCharacteristics( BluetoothManager.serviceId, this.targetWriteUUID, this.targetNotifyUUID ); }).then(() => { return BluetoothManager.subscribeNotify(); }).then(() => { this.connectedMsg = '连接成功,开始接收数据'; BluetoothManager.onData = (res) => { // res.value 是 ArrayBuffer,需要转成16进制字符串方便查看 const data = this.abToHex(res.value); console.log('收到数据:', data); }; // 发送一条查询指令,具体协议按设备文档来 const buffer = BluetoothManager.stringToArrayBuffer('AT+GETDATA'); BluetoothManager.write(buffer); }).catch((err) => { console.error('连接流程失败', err); uni.showToast({ title: '连接失败', icon: 'none' }); }); }, abToHex(buffer) { const dataView = new DataView(buffer); let hex = ''; for (let i = 0; i < dataView.byteLength; i++) { const val = dataView.getUint8(i).toString(16); hex += val.length === 1 ? '0' + val : val; } return hex; } } }; </script>

这段代码看起来长,但逻辑是一条Promise链:初始化 -> 扫描 -> 连接 -> 找服务 -> 找特征值 -> 订阅notify -> 写指令。每一步都等着上一步成功返回,任何一个环节失败都会跑到catch里,方便排查。

注意targetServiceUUID那几个值,是我从某款体脂秤的协议里随手写的,不是通用值。每个人的项目必须替换成自己设备厂商协议文档里的UUID,直接照抄肯定连不上。

4. 开发中的坑与排错实录

4.1 iOS搜索与连接的坑

iOS端我遇到的第一个坑是权限弹窗崩溃。刚开始测试时,手机一调用openBluetoothAdapter就直接闪退,检查日志才发现Info.plist里缺少NSBluetoothAlwaysUsageDescription。加上描述之后正常弹窗,问题解决。

iOS第二个坑是deviceId的稳定性。前面说过iOS的deviceId是系统生成的UUID,备份恢复或系统蓝牙重置后可能变。有一段时间我们的测试同学反馈:同一个体脂秤,昨天还能连,今天搜到同一个设备名称,点连接就一直转圈。排查后发现其实系统给这个设备的UUID变了,旧缓存数据没清,App用旧UUID去连接,当然连不上。解决方法是每次进入搜索页都刷新设备列表,不要用本地缓存的deviceId直接连。

iOS第三个坑是扫描参数。如果调startBluetoothDevicesDiscovery时传了services参数按服务UUID过滤,某些设备会搜不到,原因在于设备广播包里的service UUID和GATT服务表里的UUID不一定完全一致,有的设备广播包没广播service。建议App端扫描时不要传services过滤器,搜到设备后通过设备名称或RSSI判断目标。

iOS连接超时也是个常见问题。createBLEConnection默认没有超时时间,有时候设备信号不好,连接请求挂在那里十几秒没反应。建议显式传timeout参数,一般设10秒左右。

4.2 Android适配的坑

Android端的坑比iOS多,主要是权限和厂商限制。

先说权限。Android 6到11,如果没有定位权限,搜索功能完全没反应,但不一定会报错。我在某款华为平板上遇到过startBluetoothDevicesDiscovery返回success,但onBluetoothDeviceFound一次都不回调的情况,排查了一圈才发现是定位没开。后来我在搜索前加了一个检查逻辑,如果检测到Android平台,先确认定位服务是否可用,不可用就提示用户去设置里打开。

Android 12以上的机型,如果没有BLE相关权限,openBluetoothAdapter会返回错误。这里需要检查manifest里有没有BLUETOOTH_SCANBLUETOOTH_CONNECT权限。HBuilderX云打包时,如果只勾了老的蓝牙权限,在Android 12新机上就可能失败。

国内ROM的适配问题更头疼。部分机型在锁屏状态下会限制蓝牙扫描,还有机型的省电策略会杀掉后台蓝牙连接服务。如果你的App需要长时间维持蓝牙连接,建议引导用户把App加入电池优化白名单,否则息屏一会连接就断了。

还有一个小坑是扫描的重复注册。onBluetoothDeviceFound如果注册了两次,一次扫描会发现两条相同设备记录。我上面的封装里没有处理重复问题,实际项目里需要在页面层维护一个去重Map,以deviceId为key,只保留第一次出现的记录。

4.3 数据收发与MTU问题

BLE的数据传输不是无限长的。默认MTU是23字节,刨掉3个字节的BLE协议头,应用层一次最多传20字节。Android 8以上系统能协商MTU到更大,uni-app的底层会自动做一部分MTU协商,但iOS端相对稳定在185甚至更高。设备端能力不同,如果你写的数据超过MTU,底层就会报错。

解决办法是应用层分包发送。我见过很多人写write时直接传一个几十字节的命令,结果iOS上报错,Android上静默失败,这在BLE开发里算是经典问题了。我在实际项目里写了一个通用的分包工具,按20字节切分:

function splitBuffer(buffer, chunkSize) { const chunks = []; const view = new DataView(buffer); const total = view.byteLength; let offset = 0; while (offset < total) { const length = Math.min(chunkSize, total - offset); const chunk = new ArrayBuffer(length); const chunkView = new DataView(chunk); for (let i = 0; i < length; i++) { chunkView.setUint8(i, view.getUint8(offset + i)); } chunks.push(chunk); offset += length; } return chunks; }

使用时分包写入,每包之间间隔几十毫秒,避免设备端处理不过来。

notify监听位置也很容易踩坑。一定要在write之前先订阅notify,因为很多设备收到指令后就立刻开始上报数据,如果你监听得太晚,数据就丢了。实际项目里还要注意设备可能一次上报多包数据,要在onBLECharacteristicValueChange里做粘包处理,按协议包头、包尾或长度字段把完整报文拼出来。

4.4 常见问题速查表

问题现象可能原因解决办法
openBluetoothAdapter报10001手机蓝牙未开启提示用户打开蓝牙,或用uni.getBluetoothAdapterState轮询状态
openBluetoothAdapter报10012没有蓝牙权限或iOS缺隐私描述检查manifest权限,iOS检查NSBluetoothAlwaysUsageDescription
Android搜索不到设备或找到后不回调没有定位权限或定位服务关闭检查并申请定位权限,引导打开系统定位
搜索到了设备但连接失败iOS deviceId变化或设备不在广播范围重新开始搜索拿最新deviceId,靠近设备
连接成功但getBLEDeviceServices为空服务还没发现完成或设备异常延迟重试获取服务,或重新连接
收不到设备上报数据没订阅notify或监听注册太晚在write前调用notifyBLECharacteristicValueChange,state设true
write数据报错数据超过MTU,或value不是ArrayBuffer分包写入,确保ArrayBuffer类型
连接后一会就断开设备休眠、手机锁屏或厂商省电策略引导加入电池白名单,按协议维持心跳
安卓12上找不到蓝牙设备缺BLUETOOTH_SCAN/BLUETOOTH_CONNECT权限manifest里加上新权限,重新打自定义基座

5. 调试工具与经验技巧

5.1 nRF Connect:万能调试工具

做BLE开发,强烈建议手机里装一个nRF Connect。这是Nordic官方出的工具,能扮演中心设备扫描周边BLE设备,也能模拟外围设备广播。

日常调试时它的用处非常大。比如你的App连不上设备,你可以先用nRF Connect手动连一次,看看能不能正常发现服务、收发数据。如果工具能连能收数据,说明问题在App代码的某个环节;如果工具也连不上,那基本是设备端问题,就不用纠结代码了。

nRF Connect还能看设备的广播原始包、RSSI、所有服务和特征值,以及每个特征值的属性。这个对你理解设备协议特别有帮助。拿协议文档对照工具里的GATT表,就能确认你代码里写的服务UUID和特征值UUID对不对。

5.2 日志与断点技巧

uni-app开发App时,建议用真机调试,不要只在模拟器里跑。蓝牙是硬件能力,模拟器很难模拟出真实设备的连接行为。真正机调试注意看好console日志,但onBluetoothDeviceFound回调里的信息量比较大,建议把devices数组原样打印出来,看每个设备的name、localName、RSSI、serviceIds,这些字段是判断设备是否目标设备的关键。

如果你的设备有数据解析错误,建议在onBLECharacteristicValueChange里不要直接解析,而是先把res.value转成十六进制字符串打出来。这样至少能确认数据通不通,再谈解析对不对。上面代码里的abToHex方法可以直接复用。

5.3 最后几点个人体会

这个项目做完,我最深的感受是:BLE开发的核心不在API,而在你对“状态”的把控。蓝牙连接不是一锤子买卖,整个过程有初始化状态、扫描状态、连接状态、服务发现状态,任何一个环节被打断,下一次操作就可能卡住。所以封装BluetoothManager的时候,一定要把状态变量维护好,连接断开后及时把deviceId清空,扫描失败后要允许重试,否则App跑一段时间会出现“能扫到但连不上”之类的玄学问题。

另外就是一定要让用户有反馈路径。我加了一个日志导出功能,所有蓝牙关键操作都写进本地日志,用户遇到问题可以直接把日志发过来。定位问题和改bug的效率比靠用户口头描述高好几倍。

如果你正在做类似的项目,建议先把流程跑通,再优化体验。先用一个固定设备、一套写死的UUID,把扫描、连接、收发数据的闭环跑起来,之后再考虑多设备兼容、断线重连、防重入这些细节。BLE开发的复杂度是层层展开的,刚开始就追求完美,反而容易陷在坑里出不来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询