Android BLE 的 notify 和 indicate區(qū)別解析
做 BLE 的時(shí)候,很多人第一次看到 notify 和 indicate,會(huì)覺得它們看起來差不多。
表面上也確實(shí)差不多。兩者都是設(shè)備主動(dòng)往手機(jī)推數(shù)據(jù),Android 端通常都會(huì)在 onCharacteristicChanged() 里收到回調(diào)。你如果只看上層現(xiàn)象,很容易把它們理解成“兩個(gè)名字不同的通知模式”。
但真寫項(xiàng)目,尤其是設(shè)備協(xié)議稍微復(fù)雜一點(diǎn)以后,notify 和 indicate 的區(qū)別并不只是名詞差異。它們背后對(duì)應(yīng)的是兩種不同的可靠性模型,而這個(gè)差異會(huì)直接影響你的收消息穩(wěn)定性、吞吐量、延遲,甚至?xí)绊懩愕降自撛趺丛O(shè)計(jì)協(xié)議。
所以這篇不講太泛的 BLE 概念,就講一個(gè)實(shí)際問題:notify 和 indicate 到底差在哪,Android 端該怎么理解,項(xiàng)目里又該怎么選。
先說結(jié)論
如果只壓成一句話:
notify更快,更輕,但不保證每條都被確認(rèn)indicate更穩(wěn),更重,每一條都需要對(duì)端確認(rèn)
這句話基本對(duì),但還不夠你寫項(xiàng)目。
因?yàn)?Android 開發(fā)里最容易誤判的地方,不是“不知道它們有區(qū)別”,而是不知道這個(gè)區(qū)別會(huì)影響協(xié)議設(shè)計(jì)。
從 BLE 協(xié)議語義看
BLE GATT 里,characteristic 的值變化可以通過 server 主動(dòng)推送給 client。這里主要有兩種方式:
- Notification
- Indication
Notification 的特點(diǎn)是 server 發(fā)出去就發(fā)出去了,不要求 client 回一個(gè)鏈路層確認(rèn)。Indication 的特點(diǎn)是 server 發(fā)出去以后,要等 client 確認(rèn),這條鏈路才算完成。
也就是說,indicate 不是比 notify “高級(jí)一點(diǎn)”,而是它本身就多了一層確認(rèn)語義。
這個(gè)差異非常關(guān)鍵。
因?yàn)樗馕吨?/p>
notify更適合高頻狀態(tài)流indicate更適合關(guān)鍵狀態(tài)或關(guān)鍵事件
如果你把高頻數(shù)據(jù)流全做成 indicate,鏈路會(huì)被確認(rèn)機(jī)制拖慢。
如果你把關(guān)鍵確認(rèn)消息全做成 notify,鏈路又可能在邊界場(chǎng)景下不夠穩(wěn)。
Android 端為什么看起來差不多
很多人會(huì)混淆,是因?yàn)?Android 端最后收消息的回調(diào)長得一樣:
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
Log.d("BLE", "received=${value.joinToString(" ") { "%02X".format(it) }}")
}不管底層是 notify 還是 indicate,最終都可能進(jìn)這個(gè)回調(diào)。
所以如果你只從 Android API 表層去看,會(huì)覺得它們沒有本質(zhì)區(qū)別。
但這只是因?yàn)?Android 把“收到值變化”統(tǒng)一封裝成了同一個(gè)回調(diào),不代表底層行為一樣。
真正的差異發(fā)生在設(shè)備側(cè)和鏈路語義上,而不是發(fā)生在你收到回調(diào)的那一刻。
打開 notify 和 indicate,代碼為什么也很像
在 Android 端,開啟兩者的流程也非常接近。通常都是兩步:
- 本地調(diào)用
setCharacteristicNotification() - 寫
CCCDdescriptor
關(guān)鍵差別在于你給 CCCD 寫的值不同。
開啟 notify
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
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)真正的區(qū)別就在:
ENABLE_NOTIFICATION_VALUEENABLE_INDICATION_VALUE
也就是說,Android 端不是通過不同回調(diào)區(qū)分,而是通過寫不同的 descriptor 值去告訴設(shè)備:你應(yīng)該用哪種方式推數(shù)據(jù)。
什么時(shí)候更適合用 notify
notify 更適合這些場(chǎng)景:
- 高頻傳感器數(shù)據(jù)
- 實(shí)時(shí)狀態(tài)流
- 音頻或流式數(shù)據(jù)
- 變化很快、允許丟個(gè)別包的場(chǎng)景
原因很簡(jiǎn)單,它輕。
沒有每條都要確認(rèn)的開銷,設(shè)備可以更快地往上推。
所以如果你有一個(gè)設(shè)備每隔幾十毫秒就上報(bào)一次狀態(tài),或者持續(xù)推一段連續(xù)數(shù)據(jù),通常更適合 notify。
比如耳機(jī)、電量變化、佩戴狀態(tài)流、某些實(shí)時(shí)遙測(cè)數(shù)據(jù),這類都更適合用 notify。
但它的問題也很明確:如果你把所有東西都丟給 notify,你就默認(rèn)接受一個(gè)前提,鏈路不會(huì)幫你逐條確認(rèn)。
所以 notify 適合“多、快、可持續(xù)”的數(shù)據(jù),不適合“必須一條不丟”的關(guān)鍵控制確認(rèn)。
什么時(shí)候更適合用 indicate
indicate 更適合這些場(chǎng)景:
- 關(guān)鍵狀態(tài)變更確認(rèn)
- 必須可靠到達(dá)的結(jié)果回包
- 低頻但重要的控制響應(yīng)
- 某些升級(jí)、配對(duì)、鑒權(quán)類消息
因?yàn)?indicate 的特點(diǎn)不是快,而是它自帶確認(rèn)語義。
如果設(shè)備要告訴 App 一個(gè)特別關(guān)鍵的狀態(tài),比如:
- 配置寫入成功
- 某個(gè)模式切換已完成
- 升級(jí)狀態(tài)切換
- 某一步校驗(yàn)通過
這種時(shí)候 indicate 往往比 notify 更合理。
因?yàn)檫@類消息一旦漏掉,App 側(cè)狀態(tài)機(jī)就可能直接走歪。
真正的區(qū)別不是“哪個(gè)更好”,而是“你在傳什么”
很多 BLE 項(xiàng)目寫得不穩(wěn),本質(zhì)上不是不會(huì)調(diào) API,而是沒有把數(shù)據(jù)按語義分層。
比如把高頻狀態(tài)流做成 indicate,結(jié)果整條鏈路被確認(rèn)機(jī)制拖得很慢。
或者把關(guān)鍵確認(rèn)消息做成 notify,結(jié)果某次狀態(tài)切換沒收到,App 側(cè)以為設(shè)備沒響應(yīng)。
這就是為什么 notify 和 indicate 的區(qū)別不能只停在一句“一個(gè)快一個(gè)穩(wěn)”。
更準(zhǔn)確一點(diǎn)的說法應(yīng)該是:
notify面向數(shù)據(jù)流indicate面向關(guān)鍵狀態(tài)
如果你把這個(gè)邊界想清楚,協(xié)議設(shè)計(jì)會(huì)穩(wěn)很多。
Android 端最容易踩的坑
1. 以為setCharacteristicNotification()就夠了
很多人這樣寫:
gatt.setCharacteristicNotification(characteristic, true)
然后發(fā)現(xiàn)死活收不到數(shù)據(jù)。
原因通常不是 notify 和 indicate 的區(qū)別,而是你根本沒寫 CCCD。
這一步不做,設(shè)備側(cè)很多時(shí)候不會(huì)真正開始推送。
2. 寫錯(cuò)了 descriptor 值
如果設(shè)備 characteristic 支持的是 indicate,你卻寫了 ENABLE_NOTIFICATION_VALUE,那鏈路很可能就不工作。
反過來也一樣。
所以這件事不能憑感覺,必須看設(shè)備協(xié)議文檔和 characteristic property。
3. 不看 characteristic 的 property
不是所有 characteristic 都同時(shí)支持 notify 和 indicate。
在 Android 端最好先檢查一下:
val properties = characteristic.properties
val supportsNotify =
properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0
val supportsIndicate =
properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0如果設(shè)備只支持其中一種,你寫另一種 descriptor 值,本來就不對(duì)。
4. 以為收不到數(shù)據(jù)一定是 Android 的問題
很多時(shí)候 Android 端代碼都寫對(duì)了,但還是沒數(shù)據(jù)。
這時(shí)候問題可能在設(shè)備側(cè):
- 設(shè)備沒真正開啟推送
- 設(shè)備需要先發(fā)初始化命令
- 設(shè)備只有在某種模式下才會(huì)上報(bào)
- 設(shè)備協(xié)議里 notify/indicate 的使用條件有前置狀態(tài)
也就是說,onCharacteristicChanged() 沒回調(diào),不等于一定是手機(jī)問題。
在協(xié)議設(shè)計(jì)里怎么選
如果你自己能參與設(shè)備協(xié)議設(shè)計(jì),我建議按這個(gè)思路來分:
高頻、連續(xù)、允許局部丟失的數(shù)據(jù),用 notify。
關(guān)鍵、低頻、必須確認(rèn)的狀態(tài),用 indicate。
比如:
- 設(shè)備實(shí)時(shí)傳感器流,優(yōu)先
notify - ANC 模式切換結(jié)果,優(yōu)先
indicate - 耳機(jī)狀態(tài)周期性同步,優(yōu)先
notify - OTA 某一步完成確認(rèn),優(yōu)先
indicate
這樣協(xié)議語義會(huì)更清晰,Android 端也更容易圍繞不同消息建立狀態(tài)機(jī)。
項(xiàng)目里最實(shí)用的一句判斷
如果你現(xiàn)在在做 BLE 項(xiàng)目,遇到“為什么有些消息總感覺不穩(wěn)”,最先該問自己的不是:
“Android 這個(gè)回調(diào)是不是有 bug?”
而是:
“這條消息本來就應(yīng)該用 notify,還是 indicate?”
因?yàn)楹芏嗍障栴},根源不是 API 沒調(diào)對(duì),而是協(xié)議語義一開始就選錯(cuò)了。
結(jié)尾
notify 和 indicate 在 Android 端看起來很像,都是通過 characteristic 變化把數(shù)據(jù)推上來。但真正的差異不在回調(diào),而在底層的確認(rèn)語義。
notify 更適合快一點(diǎn)、連續(xù)一點(diǎn)的數(shù)據(jù)。indicate 更適合慢一點(diǎn)、但必須穩(wěn)一點(diǎn)的關(guān)鍵狀態(tài)。
所以這個(gè)問題最后其實(shí)不是“它們有什么區(qū)別”,而是:
你這條消息,到底是在傳數(shù)據(jù)流,還是在傳一個(gè)不能丟的狀態(tài)。
到此這篇關(guān)于Android BLE 的 notify 和 indicate 到底有什么區(qū)別的文章就介紹到這了,更多相關(guān)Android BLE notify 和 indicate 區(qū)別內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Ubuntu中為Android增加硬件抽象層(HAL)模塊訪問Linux內(nèi)核驅(qū)動(dòng)程序
本文主要介紹在Ubuntu上為Android HAL模塊訪問Linux內(nèi)核驅(qū)動(dòng)程序,這里給大家提供方法和一個(gè)小的測(cè)試程序代碼,以及常遇到的問題和解決方法,有需要的小伙伴可以參考下2016-08-08
Android中使用Spinner實(shí)現(xiàn)下拉列表功能
Spinner是一個(gè)列表選擇框,會(huì)在用戶選擇后,展示一個(gè)列表供用戶進(jìn)行選擇。下面通過本文給大家實(shí)例詳解android中使用Spinner實(shí)現(xiàn)下拉列表功能,一起看看吧2017-04-04
Android?Flutter在點(diǎn)擊事件上添加動(dòng)畫效果實(shí)現(xiàn)全過程
這篇文章主要給大家介紹了關(guān)于Android?Flutter在點(diǎn)擊事件上添加動(dòng)畫效果實(shí)現(xiàn)的相關(guān)資料,通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)Android具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-03-03
Android使用Sqlite存儲(chǔ)數(shù)據(jù)用法示例
這篇文章主要介紹了Android使用Sqlite存儲(chǔ)數(shù)據(jù)的方法,結(jié)合實(shí)例形式分析了Android操作SQLite數(shù)據(jù)庫的相關(guān)步驟與操作技巧,需要的朋友可以參考下2016-11-11
Android應(yīng)用中使用ListView來分頁顯示刷新的內(nèi)容
這篇文章主要介紹了Android應(yīng)用中使用ListView來分頁顯示刷新的內(nèi)容的方法,展示了一個(gè)點(diǎn)擊按鈕進(jìn)行刷新的實(shí)例以及下拉刷新分頁顯示的要點(diǎn)解析,需要的朋友可以參考下2016-04-04
Android 模仿QQ側(cè)滑刪除ListView功能示例
這篇文章主要介紹了Android 模仿QQ側(cè)滑刪除ListView功能示例,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-03-03
關(guān)于Android Activity之間跳轉(zhuǎn)問題(Intent)
這篇文章主要介紹了Android Activity之間跳轉(zhuǎn)Intent,當(dāng)一個(gè)Acitivity需要啟動(dòng)另一個(gè)Activity時(shí),通過Intent來表達(dá)自己的意圖,告知系統(tǒng)啟動(dòng)哪個(gè)Activity,本文給大家詳細(xì)講解,需要的朋友可以參考下2022-10-10
Android Retrofit文件下載進(jìn)度顯示問題的解決方法
這篇文章主要為大家詳細(xì)介紹了Android Retrofit文件下載進(jìn)度顯示問題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01

