做 BLE 的时候,很多人第一次看到 notify 和 indicate,会觉得它们看起来差不多。
表面上也确实差不多。两者都是设备主动往手机推数据,Android 端通常都会在 onCharacteristicChanged() 里收到回调。你如果只看上层现象,很容易把它们理解成"两个名字不同的通知模式"。
但真写项目,尤其是设备协议稍微复杂一点以后,notify 和 indicate 的区别并不只是名词差异。它们背后对应的是两种不同的可靠性模型,而这个差异会直接影响你的收消息稳定性、吞吐量、延迟,甚至会影响你到底该怎么设计协议。
所以这篇不讲太泛的 BLE 概念,就讲一个实际问题:notify 和 indicate 到底差在哪,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 端,开启两者的流程也非常接近。通常都是两步:
- 本地调用
setCharacteristicNotification() - 写
CCCDdescriptor
关键差别在于你给 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_VALUEENABLE_INDICATION_VALUE
也就是说,Android 端不是通过不同回调区分,而是通过写不同的 descriptor 值去告诉设备:你应该用哪种方式推数据。
什么时候更适合用 notify
notify 更适合这些场景:
- 高频传感器数据
- 实时状态流
- 音频或流式数据
- 变化很快、允许丢个别包的场景
原因很简单,它轻。
没有每条都要确认的开销,设备可以更快地往上推。
所以如果你有一个设备每隔几十毫秒就上报一次状态,或者持续推一段连续数据,通常更适合 notify。
比如耳机、电量变化、佩戴状态流、某些实时遥测数据,这类都更适合用 notify。
但它的问题也很明确:如果你把所有东西都丢给 notify,你就默认接受一个前提,链路不会帮你逐条确认。
所以 notify 适合"多、快、可持续"的数据,不适合"必须一条不丢"的关键控制确认。
什么时候更适合用 indicate
indicate 更适合这些场景:
- 关键状态变更确认
- 必须可靠到达的结果回包
- 低频但重要的控制响应
- 某些升级、配对、鉴权类消息
因为 indicate 的特点不是快,而是它自带确认语义。
如果设备要告诉 App 一个特别关键的状态,比如:
- 配置写入成功
- 某个模式切换已完成
- 升级状态切换
- 某一步校验通过
这种时候 indicate 往往比 notify 更合理。
因为这类消息一旦漏掉,App 侧状态机就可能直接走歪。
真正的区别不是"哪个更好",而是"你在传什么"
很多 BLE 项目写得不稳,本质上不是不会调 API,而是没有把数据按语义分层。
比如把高频状态流做成 indicate,结果整条链路被确认机制拖得很慢。
或者把关键确认消息做成 notify,结果某次状态切换没收到,App 侧以为设备没响应。
这就是为什么 notify 和 indicate 的区别不能只停在一句"一个快一个稳"。
更准确一点的说法应该是:
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 没调对,而是协议语义一开始就选错了。
结尾
notify 和 indicate 在 Android 端看起来很像,都是通过 characteristic 变化把数据推上来。但真正的差异不在回调,而在底层的确认语义。
notify 更适合快一点、连续一点的数据。
indicate 更适合慢一点、但必须稳一点的关键状态。
所以这个问题最后其实不是"它们有什么区别",而是:
你这条消息,到底是在传数据流,还是在传一个不能丢的状态。