Android BLE 为什么连上了却收不到数据

BLE 里有一种情况特别烦。

设备能扫到,也能连上,onConnectionStateChange() 进了,discoverServices() 也成功了,日志一眼看过去甚至有种"都好了"的错觉。结果你等了半天,onCharacteristicChanged() 根本不来。

这种问题最容易把人带偏。因为从表面上看,蓝牙连接已经成功了,直觉上就会觉得"那剩下不就是等设备发数据吗"。但实际项目里,连接成功和数据真的能回来,中间差得还挺远。

我后来碰这种问题多了,基本都会先提醒自己一句:连上了,不代表链路已经通了。

BLE 这里最容易被忽略的一点,就是 GATT 连接只是把通道建起来。它不负责替你把通知打开,不负责替你初始化设备协议,也不负责保证你现在监听的就是那个真正会推数据的 characteristic。你如果把"连接成功"和"可以收消息"当成一回事,后面就很容易陷进去。

最常见的误区,就是以为这句代码做完事情就结束了:

kotlin 复制代码
gatt.setCharacteristicNotification(characteristic, true)

很多文章也喜欢写到这里就停,给人的感觉像是"打开通知完成"。其实这一步更像是告诉 Android 本地蓝牙栈:我准备监听这个 characteristic 的变化了。它不等于设备端已经收到命令,也不等于设备现在真的会开始推。

真正关键的通常是下一步,也就是 CCCD

kotlin 复制代码
val cccd = characteristic.getDescriptor(
    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
)

cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccd)

很多"连上了却没数据"的问题,说到底就是这里没做对。要么根本没写 descriptor,要么写错了值,要么设备要的是 indicate,你却按 notify 去开。更常见一点的,是 descriptor 还没真正写成功,后面的业务命令已经抢跑了。

这个顺序问题很容易被低估。因为代码写起来就是顺手几行:

kotlin 复制代码
gatt.setCharacteristicNotification(characteristic, true)
gatt.writeDescriptor(cccd)
sendInitCommand()

看上去很顺,实际经常出事。writeDescriptor() 本身就是异步的,你如果还没等它成功,就先把初始化命令发给设备,设备那边即使已经回了,你也不一定接得住。最后从手机视角看就是一句很熟悉的话:明明都连上了,就是收不到数据。

更稳一点的写法反而很朴素。等 onDescriptorWrite() 成功以后,再继续下一步。

kotlin 复制代码
override fun onDescriptorWrite(
    gatt: BluetoothGatt,
    descriptor: BluetoothGattDescriptor,
    status: Int
) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        sendInitCommand()
    }
}

这时候你至少能确定,通知链路是真的被打开了,而不是只在本地做了个姿态。

还有一种情况也特别常见,就是 characteristic 本身就拿错了。很多 BLE 设备不是一个 characteristic 走天下,通常会拆成命令通道、响应通道、数据通道。你如果把"用来发命令"的那个 characteristic 拿去开通知,逻辑上未必会立刻报错,但它本来就不是用来上报数据的。结果你在 Android 端等回调,当然等不到。

这个时候最笨但最有效的办法,就是先看 property,不要靠猜。

kotlin 复制代码
val properties = characteristic.properties

val supportsNotify =
    properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0

val supportsIndicate =
    properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0

如果一个 characteristic 连 notify / indicate 都不支持,那后面不用看了,方向就不对。

但 BLE 里更麻烦的一层,还不在 GATT,而在设备协议本身。

很多设备并不是"连上就推"。真实逻辑更像这样:

连接成功,服务发现成功,通知链路打开,App 再发一条初始化命令,设备收到以后才进入上报状态。

这一类设备特别容易把人带偏,因为从 Android 侧看,BLE 每一步都成功了,可就是没数据。你如果一直在 BLE 层绕,会觉得很玄。其实问题根本不是 BLE 没通,而是设备协议还没被真正激活。

所以这类问题最好分两层看。

第一层是 BLE 链路有没有通。也就是服务有没有发现到,通知有没有打开,descriptor 有没有写成功。

第二层是设备协议有没有进入工作状态。也就是你该发的初始化命令发了没有,设备是否真的开始上报,当前是不是对的模式。

这两层一旦混在一起,就很容易出现一种假象:所有 BLE API 都成功了,但业务就是没反应。

我现在自己排这种问题,基本不会再问"为什么没数据",而是会把状态拆开看。至少会分成这几段:

kotlin 复制代码
enum class BleReadyState {
    Disconnected,
    Connected,
    ServicesReady,
    NotificationReady,
    ProtocolReady
}

这个状态拆分看起来普通,但很有用。因为它能逼你把"连接建立""通知可用""协议 ready"这三件事分开。只要你代码里没有这几个边界,最后所有异常都会被压成同一句话:BLE 不稳定。

实际上,很多 BLE 所谓的不稳定,根本不是链路一直在抖,而是你太早把系统当成 ready 了。

如果把顺序收紧一点,比较稳的流程通常是这样的:

先连上设备。

再发现服务。

拿到正确的 characteristic。

打开本地 notification。

CCCD

onDescriptorWrite() 成功。

再发初始化命令。

最后等设备开始上报。

这套流程一点也不酷,甚至显得有点啰嗦,但它比"连上之后一口气往下调"稳得多。BLE 这类项目到后面,拼的往往不是谁更会写 API,而是谁更肯老老实实按顺序做事。

如果你现在就卡在"连上了却收不到数据",最值得先查的不是 Android 蓝牙栈,也不是设备固件,而是下面这几件事:

你拿到的 characteristic 对不对。
CCCD 有没有写。

写的是 notify 还是 indicate

descriptor 写成功之前是不是已经抢跑发命令了。

设备是不是还要求一条初始化命令,才会真正开始推数据。

很多时候,把这几件事捋顺,问题就已经解决了大半。

所以这类问题最后其实不是"为什么连上了没数据",而是你一开始就把两个阶段混成了一件事。
连接成功只是开始,数据链路 ready 是另一回事。

相关推荐
程序员陆业聪1 小时前
当AI学会了混淆代码:LLM辅助混淆 vs R8,Android安全的下一个十字路口
android
yubin12855709231 小时前
mysql正则函数REGEXP
android·数据库·mysql
我命由我123451 小时前
Android Framework P2 - 开机启动 Zygote 进程、Zygote 的预加载机制
android·java·开发语言·python·java-ee·intellij-idea·zygote
我命由我123451 小时前
Android Framework P1 - 低配学习 Framework 方案、开机启动 Init 进程
android·c语言·c++·学习·android jetpack·android-studio·android runtime
aqi002 小时前
FFmpeg开发笔记(一百零二)国产的音视频移动开源工具FFmpegAndroid
android·ffmpeg·kotlin·音视频·直播·流媒体
星间都市山脉2 小时前
Android 谷歌 CTS 完整测试
android
nianniannnn2 小时前
快应用day2项目架构
android·快应用
用户83352502537853 小时前
ViewModel详细解析
android
问心无愧05133 小时前
ctf show web入门91
android·前端·笔记
YF02113 小时前
Android App 高效升级指南:OkDownload 多线程断点续传与全版本安装适配
android·okhttp·app