Android BLE 的 notify 和 indicate 到底有什么区别

做 BLE 的时候,很多人第一次看到 notifyindicate,会觉得它们看起来差不多。

表面上也确实差不多。两者都是设备主动往手机推数据,Android 端通常都会在 onCharacteristicChanged() 里收到回调。你如果只看上层现象,很容易把它们理解成"两个名字不同的通知模式"。

但真写项目,尤其是设备协议稍微复杂一点以后,notifyindicate 的区别并不只是名词差异。它们背后对应的是两种不同的可靠性模型,而这个差异会直接影响你的收消息稳定性、吞吐量、延迟,甚至会影响你到底该怎么设计协议。

所以这篇不讲太泛的 BLE 概念,就讲一个实际问题:notifyindicate 到底差在哪,Android 端该怎么理解,项目里又该怎么选。

先说结论

如果只压成一句话:

  • notify 更快,更轻,但不保证每条都被确认
  • indicate 更稳,更重,每一条都需要对端确认

这句话基本对,但还不够你写项目。

因为 Android 开发里最容易误判的地方,不是"不知道它们有区别",而是不知道这个区别会影响协议设计

从 BLE 协议语义看

BLE GATT 里,characteristic 的值变化可以通过 server 主动推送给 client。这里主要有两种方式:

  • Notification
  • Indication

Notification 的特点是 server 发出去就发出去了,不要求 client 回一个链路层确认。
Indication 的特点是 server 发出去以后,要等 client 确认,这条链路才算完成。

也就是说,indicate 不是比 notify "高级一点",而是它本身就多了一层确认语义。

这个差异非常关键。

因为它意味着:

  • notify 更适合高频状态流
  • indicate 更适合关键状态或关键事件

如果你把高频数据流全做成 indicate,链路会被确认机制拖慢。

如果你把关键确认消息全做成 notify,链路又可能在边界场景下不够稳。

Android 端为什么看起来差不多

很多人会混淆,是因为 Android 端最后收消息的回调长得一样:

kotlin 复制代码
override fun onCharacteristicChanged(
    gatt: BluetoothGatt,
    characteristic: BluetoothGattCharacteristic,
    value: ByteArray
) {
    Log.d("BLE", "received=${value.joinToString(" ") { "%02X".format(it) }}")
}

不管底层是 notify 还是 indicate,最终都可能进这个回调。

所以如果你只从 Android API 表层去看,会觉得它们没有本质区别。

但这只是因为 Android 把"收到值变化"统一封装成了同一个回调,不代表底层行为一样。

真正的差异发生在设备侧和链路语义上,而不是发生在你收到回调的那一刻。

打开 notify 和 indicate,代码为什么也很像

在 Android 端,开启两者的流程也非常接近。通常都是两步:

  1. 本地调用 setCharacteristicNotification()
  2. CCCD descriptor

关键差别在于你给 CCCD 写的值不同。

开启 notify

kotlin 复制代码
val characteristic = service.getCharacteristic(NOTIFY_UUID)
gatt.setCharacteristicNotification(characteristic, true)

val cccd = characteristic.getDescriptor(
    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
)
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccd)

开启 indicate

kotlin 复制代码
val characteristic = service.getCharacteristic(INDICATE_UUID)
gatt.setCharacteristicNotification(characteristic, true)

val cccd = characteristic.getDescriptor(
    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
)
cccd.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
gatt.writeDescriptor(cccd)

真正的区别就在:

  • ENABLE_NOTIFICATION_VALUE
  • ENABLE_INDICATION_VALUE

也就是说,Android 端不是通过不同回调区分,而是通过写不同的 descriptor 值去告诉设备:你应该用哪种方式推数据。

什么时候更适合用 notify

notify 更适合这些场景:

  • 高频传感器数据
  • 实时状态流
  • 音频或流式数据
  • 变化很快、允许丢个别包的场景

原因很简单,它轻。

没有每条都要确认的开销,设备可以更快地往上推。

所以如果你有一个设备每隔几十毫秒就上报一次状态,或者持续推一段连续数据,通常更适合 notify

比如耳机、电量变化、佩戴状态流、某些实时遥测数据,这类都更适合用 notify

但它的问题也很明确:如果你把所有东西都丢给 notify,你就默认接受一个前提,链路不会帮你逐条确认。

所以 notify 适合"多、快、可持续"的数据,不适合"必须一条不丢"的关键控制确认。

什么时候更适合用 indicate

indicate 更适合这些场景:

  • 关键状态变更确认
  • 必须可靠到达的结果回包
  • 低频但重要的控制响应
  • 某些升级、配对、鉴权类消息

因为 indicate 的特点不是快,而是它自带确认语义。

如果设备要告诉 App 一个特别关键的状态,比如:

  • 配置写入成功
  • 某个模式切换已完成
  • 升级状态切换
  • 某一步校验通过

这种时候 indicate 往往比 notify 更合理。

因为这类消息一旦漏掉,App 侧状态机就可能直接走歪。

真正的区别不是"哪个更好",而是"你在传什么"

很多 BLE 项目写得不稳,本质上不是不会调 API,而是没有把数据按语义分层。

比如把高频状态流做成 indicate,结果整条链路被确认机制拖得很慢。

或者把关键确认消息做成 notify,结果某次状态切换没收到,App 侧以为设备没响应。

这就是为什么 notifyindicate 的区别不能只停在一句"一个快一个稳"。

更准确一点的说法应该是:

  • notify 面向数据流
  • indicate 面向关键状态

如果你把这个边界想清楚,协议设计会稳很多。

Android 端最容易踩的坑

1. 以为 setCharacteristicNotification() 就够了

很多人这样写:

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

然后发现死活收不到数据。

原因通常不是 notify 和 indicate 的区别,而是你根本没写 CCCD

这一步不做,设备侧很多时候不会真正开始推送。

2. 写错了 descriptor 值

如果设备 characteristic 支持的是 indicate,你却写了 ENABLE_NOTIFICATION_VALUE,那链路很可能就不工作。

反过来也一样。

所以这件事不能凭感觉,必须看设备协议文档和 characteristic property。

3. 不看 characteristic 的 property

不是所有 characteristic 都同时支持 notify 和 indicate。

在 Android 端最好先检查一下:

kotlin 复制代码
val properties = characteristic.properties

val supportsNotify =
    properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0

val supportsIndicate =
    properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0

如果设备只支持其中一种,你写另一种 descriptor 值,本来就不对。

4. 以为收不到数据一定是 Android 的问题

很多时候 Android 端代码都写对了,但还是没数据。

这时候问题可能在设备侧:

  • 设备没真正开启推送
  • 设备需要先发初始化命令
  • 设备只有在某种模式下才会上报
  • 设备协议里 notify/indicate 的使用条件有前置状态

也就是说,onCharacteristicChanged() 没回调,不等于一定是手机问题。

在协议设计里怎么选

如果你自己能参与设备协议设计,我建议按这个思路来分:

高频、连续、允许局部丢失的数据,用 notify

关键、低频、必须确认的状态,用 indicate

比如:

  • 设备实时传感器流,优先 notify
  • ANC 模式切换结果,优先 indicate
  • 耳机状态周期性同步,优先 notify
  • OTA 某一步完成确认,优先 indicate

这样协议语义会更清晰,Android 端也更容易围绕不同消息建立状态机。

项目里最实用的一句判断

如果你现在在做 BLE 项目,遇到"为什么有些消息总感觉不稳",最先该问自己的不是:

"Android 这个回调是不是有 bug?"

而是:

"这条消息本来就应该用 notify,还是 indicate?"

因为很多收消息问题,根源不是 API 没调对,而是协议语义一开始就选错了。

结尾

notifyindicate 在 Android 端看起来很像,都是通过 characteristic 变化把数据推上来。但真正的差异不在回调,而在底层的确认语义。

notify 更适合快一点、连续一点的数据。
indicate 更适合慢一点、但必须稳一点的关键状态。

所以这个问题最后其实不是"它们有什么区别",而是:

你这条消息,到底是在传数据流,还是在传一个不能丢的状态。

相关推荐
小码哥_常2 小时前
从MVC到MVI:一文吃透架构模式进化史
前端
豹哥学前端2 小时前
别再背“var 提升,let/const 不提升”了:揭开暂时性死区的真实面目
前端·面试
lar_slw2 小时前
k8s部署前端项目
前端·容器·kubernetes
拉拉肥_King2 小时前
Ant Design Table 横向滚动条神秘消失?我是如何一步步找到真凶的
前端·javascript
GreenTea2 小时前
DeepSeek-V4 技术报告深度分析:基础研究创新全景
前端·人工智能·后端
河阿里3 小时前
HTML5标准完全教学手册
前端·html·html5
吴声子夜歌3 小时前
Vue3——新语法
前端·javascript·vue.js
jiayong233 小时前
第 36 课:任务详情抽屉快捷改状态
开发语言·前端·javascript·vue.js·学习
FFF_634560233 小时前
通用 vue 页面 js 下载任何文件的方法
开发语言·前端·javascript