Android 車機與 BLE 設備交互全鏈路實踐指南
從廣播地址、配對綁定到隱私機制的完整理解
最近在開發車機系統與無屏 BLE 設備(比如智能冰箱)的連接功能,過程中遇到了一連串看似獨立、實則緊密關聯的問題:為什麼掃描到的地址和配對時一樣?RPA 地址到底能不能看到?解綁為什麼沒有公開 API?回連時直接用連接成功時保存的 MAC 行不行?ADB 怎麼清空配對列表?
這些問題背後其實是一套完整的 BLE 安全與隱私機制。這篇文章把整個探索過程串起來,記錄下驗證過的方法和踩過的坑,希望能幫到正在做類似工作的你。
一、設備廣播地址:你以為的“MAC”可能不是真 MAC
一開始我在連接成功後將設備的MAC保存到SP中,後續APP啓動或者藍牙開啓後直接根據整個mac直接去連接設備,
大致流程如圖:
但是有的設備是OK的,有的設備不行,後來發現不行的設備是開啓了BLE Privacy機制,無法直接根據連接時返回的mac直接連接
後來發現這涉及到 BLE(Bluetooth Low Energy)隱私保護機制 的核心設計 —— RPA(Resolvable Private Address,可解析私有地址)
首先看下BLE設備的廣播類型
BLE 廣播地址的類型
根據 BLE 規範(Core Spec Vol 6, Part B, Section 1.3),廣播包中的 AdvA(Advertiser Address) 可以是以下之一:
| 地址類型 | 是否可變 | 是否可被解析 | 説明 |
|---|---|---|---|
| Public Device Address | ❌ 固定 | ✅ 是 | 廠商燒錄的 MAC |
| Static Device Address | ❌ 固定 | ✅ 是 | 設備自定義的靜態隨機地址 |
| Resolvable Private Address (RPA) | ✅ 動態(如每15分鐘換) | ✅ 僅對有 IRK 的設備 | 隱私保護,可被配對設備解析 |
| Non-resolvable Private Address | ✅ 動態 | ❌ 否 | 完全匿名,無法追蹤也無法連接(通常用於 beacon) |
📌 大多數支持配對的智能設備(如你的冰箱)會使用 RPA 來廣播,以保護用户隱私。
傳統 MAC 地址的問題
早期 BLE 設備使用 靜態公開地址(Public Static Address),比如:
AA:BB:CC:11:22:33
這個地址是固定的、全球唯一(理論上),但也帶來嚴重隱私問題:
🕵️♂️ 攻擊者可以在商場、地鐵等公共場所通過掃描 BLE 廣播包,追蹤某個設備(比如你的手機或冰箱)的行蹤。
為了解決這個問題,BLE 4.0 引入了 Privacy Feature(隱私特性),允許設備使用 RPA 隨機變化的地址,而不是固定 MAC。
🛡️ RPA(Resolvable Private Address)是什麼?
RPA 是一種 動態變化但可被“信任設備”識別 的地址機制。
工作原理簡述:
- 配對(Bonding)時,雙方交換一個密鑰:IRK(Identity Resolving Key)。
- 此後,設備會定期(如每 15 分鐘)生成一個新的 隨機地址(RPA),該地址由 IRK + 隨機數加密生成。
- 只有擁有相同 IRK 的設備(即已配對設備)才能 “解析”這個 RPA,確認它來自同一個物理設備。
- 對未配對的第三方來説,看到的只是一個不斷變化的隨機地址,無法追蹤。
✅ 優點:既保護隱私,又不影響已配對設備之間的通信
什麼是Identify Address
-
Identity Address
是設備在配對(Pairing + Bonding)過程中交換的“真實身份”,格式為:
- Public Address(如廠商燒錄的 MAC)
- 或 Static Random Address(由設備製造商設定,固定不變)
- 這個地址 在設備生命週期內是固定的,也是 Android 系統在
BluetoothDevice.getAddress()中返回的值(對於 bonded 設備)。 - IRK(Identity Resolving Key)就是用來將 RPA 映射回這個 Identity Address 的。
📱 Android 如何處理 RPA?
-
當你和一個支持隱私特性的 BLE 設備(如現代智能冰箱)完成 配對(bonding) 後:
- Android 系統會 自動保存該設備的 IRK;
- 即使冰箱下次廣播的是一個全新的 RPA(比如
D4:E5:F6:77:88:99),Android 也能通過 IRK 識別出:“這是之前配對過的那台冰箱”。
-
此時,你在代碼中調用:
BluetoothAdapter.getDefaultAdapter().getBondedDevices()返回的
BluetoothDevice對象的.getAddress()始終是配對時的“身份地址”(Identity Address),通常是 Public 或 Static 地址(如AA:BB:CC:11:22:33),而不是當前廣播的 RPA。
✅ 所以:系統內部已經幫你完成了 RPA → Identity Address 的映射。
這就回到了為什麼直接根據掃描到的mac地址直接回連設備會失敗的問題了
❌ 為什麼不要直接用存儲的 MAC 字符串調用 getRemoteDevice(mac)?
假設你把掃描到的 MAC(如 "AA:BB:CC:11:22:33")存到 SharedPreferences,下次直接:
String savedMac = prefs.getString("fridge_mac", null);
BluetoothDevice dev = adapter.getRemoteDevice(savedMac);
dev.connectGatt(...);
getRemoteDevice(mac)僅根據地址字符串返回一個設備引用,它不保證能訪問到該設備的綁定上下文(如 IRK)。如果傳入的地址不是已配對設備的 Identity Address(例如是一個 RPA),即使物理設備已綁定,系統也無法自動解析隱私地址- 如果此時冰箱正在使用 RPA(比如廣播地址是
D4:E5:F6:77:88:99),而你傳入的是舊的 Identity Address(AA:BB:CC:...); - Android 不會自動用 IRK 去解析或關聯這個 RPA,因為
getRemoteDevice()不知道這個設備是否已配對; - 結果:連接失敗(GATT ERROR 133 或 timeout),即使物理設備就在旁邊!
💡 換句話説:getRemoteDevice() 繞過了系統的 bonding 數據庫和 IRK 解析機制。
✅ 正確做法:從 getBondedDevices() 中查找
String savedIdentityAddress = "AA:BB:CC:11:22:33"; // 這是你配對時記錄的身份地址
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
for (BluetoothDevice device : adapter.getBondedDevices()) {
if (device.getAddress().equals(savedIdentityAddress)) {
// ✅ 這個 device 對象是系統管理的 bonded 設備
// 即使冰箱當前用 RPA 廣播,系統也會自動解析並建立連接
device.connectGatt(context, false, gattCallback);
break;
}
}
這樣做的好處:
- 利用了 Android 內置的 IRK 解析能力;
- 無論冰箱當前使用什麼 RPA,系統都能正確路由到物理設備;
- 連接成功率高,符合 BLE 規範。
🔧 補充建議
- 要記錄配對時的mac地址而不是掃描到的mac地址,因為掃描的到mac可能是PRA,但是綁定時的mac一定是Identify Address
- 在配對完成後,記錄的是
device.getAddress()(即 Identity Address),這個地址在 bonding 生命週期內是穩定的; - 不要嘗試自己解析 RPA(除非你實現完整的 BLE Host 層,不推薦);
- 如果目標設備 不支持 Privacy(即始終用 Public Address),那麼
getRemoteDevice()也能工作,但為了兼容性和未來升級,仍建議走 bonded devices 路徑。
✅ 總結
| 方式 | 是否推薦 | 原因 |
|---|---|---|
getRemoteDevice(savedMac) |
❌ 不推薦 | 無法利用 IRK 解析 RPA,連接可能失敗 |
從 getBondedDevices() 查找匹配地址 |
✅ 強烈推薦 | 系統自動處理 RPA,連接可靠 |
所以,永遠優先使用系統提供的 bonded device 對象來發起連接,而不是自己構造設備對象。這不僅是最佳實踐,也是應對現代 BLE 隱私機制的必要手段。
Android 在設備綁定後,所有 API 返回的都是 Identity Address(身份地址),而不是設備當前廣播的地址。
BLE 設備可以廣播四種地址:
- Public Address:芯片的固定 MAC;
- Static Random Address:開機生成、運行期間不變的隨機地址;
- Non-Resolvable Private Address (NRPA):頻繁變化、無法追蹤的臨時地址;
- Resolvable Private Address (RPA):頻繁變化,但持有 IRK 的設備能解析回 Identity Address。
真正的 Privacy 機制 = 使用 RPA 廣播 + IRK 解析。
當你在車機上調用 device.getAddress(),如果設備已綁定,Android 會自動用 IRK 把 RPA “翻譯” 成 Identity Address 再返回給你。所以你永遠看不到那個變化的 RPA —— 這不是 bug,而是 Privacy 正常工作的表現。
✅ 驗證方法:用手機裝 nRF Connect,在未綁定狀態下掃描設備,隔 15 分鐘看地址是否變化。如果變了,説明 Privacy 生效了。
二、RPA 地址能看到嗎?怎麼獲取原始廣播地址?
既然系統把 RPA 隱藏了,那我們還能不能拿到真實的廣播地址?
答案是:只有在未綁定狀態下才可能看到。
當你調用 BluetoothLeScanner.startScan(),如果設備還沒配對,且它廣播的是 RPA,那麼 ScanResult.device.getAddress() 返回的就是這個原始 RPA(比如 D3:A1:F5:09:88:77)。但一旦你完成綁定,下次再掃,系統就會直接給你 Identity Address。
所以,不要試圖在綁定後獲取 RPA——你不需要它。Identity Address 才是穩定的設備標識,RPA 只是空中傳輸的“馬甲”。
如果你是在調試固件,建議用 nRF Connect 或藍牙嗅探器抓包;如果是開發車機 App,請完全忽略 RPA 的存在,只認 getBondedDevices() 裏的地址。
三、回連時直接用保存的 MAC 地址行不行?
早期我們圖省事,配對成功後把設備地址存下來,下次啓動直接用:
val device = adapter.getRemoteDevice(savedMac)
device.connectGatt(...)
結果某天測試新固件(啓用了 RPA)時,連接直接超時失敗。
原因很簡單:getRemoteDevice() 創建的是一個“裸設備對象”,它沒有 IRK,也不知道這個地址對應的是誰。而冰箱此刻廣播的是 RPA,根本不在 savedMac 這個地址上。
正確做法是:
val device = adapter.bondedDevices.find { it.address == savedMac }
device?.connectGatt(...)
因為 bondedDevices 裏的設備對象帶着完整的綁定上下文(包括 IRK),系統能自動把 RPA 解析出來並建立連接。
📌 記住:地址字符串相同 ≠ 設備對象等價。安全上下文才是關鍵。
2. Android 如何處理這個地址?
- 在
onLeScan()中,Android 直接把廣播包裏的 AdvA 字段原樣封裝成BluetoothDevice對象的地址; - 此時系統 還不知道這個設備是否已配對,也沒有嘗試用 IRK 去解析它(因為還沒建立 bonding 上下文);
- 所以:
device.getAddress()就是原始廣播地址(raw advertising address)。
✅ 舉例:
- 冰箱的 Identity Address 是
AA:BB:CC:11:22:33(Public); - 當前廣播使用 RPA:
D4:E5:F6:77:88:99; - 你在
onLeScan()中拿到的device.getAddress()就是"D4:E5:F6:77:88:99"; - 即使你之前已經和這台冰箱配對過,掃描回調仍然返回 RPA,因為這是物理層看到的內容。
⚠️ 這就是為什麼不能在掃描階段存儲這個地址作為設備唯一標識!
3. 那系統怎麼知道這是“老朋友”?
-
當你調用
device.createBond() 或 device.connectGatt(...)時,Android 會:
- 檢查本地是否有該 Identity Address 對應的 IRK(即是否已配對);
- 如果有,就嘗試用 IRK 解析當前 RPA;
- 如果解析成功(RPA 能還原出已知 Identity Address),就走快速重連流程(無需重新配對);
- 連接成功後,
BluetoothDevice.getAddress()在後續 API 調用中(如 GATT 回調、bonded devices 列表)會返回 Identity Address。
-
public Address和identify Address 是一回事嗎?
-
Public Address 是 Identity Address 的一種,但 Identity Address 不一定是 Public Address。
換句話説:
- Identity Address(身份地址)是一個邏輯概念,用於唯一標識一個 BLE 設備;
-
它可以是以下兩種之一:
- Public Device Address(公開地址,即傳統 MAC 地址)
- Static Random Address(靜態隨機地址)
所以:
✅ 所有 Public Address 都是 Identity Address,
❌ 但不是所有 Identity Address 都是 Public Address。
1. 什麼是 Identity Address(身份地址)?
根據 Bluetooth Core Specification(BLE 核心規範):
The Identity Address is the address used to identify a device during the pairing and bonding process. It is either:
- A Public Device Address, or
- A Static Random Address
這個地址在設備的整個生命週期中是 固定不變的,並且會和 IRK(Identity Resolving Key) 一起在配對時交換,用於後續解析 RPA(Resolvable Private Address)。
2. Public Device Address(公開地址)
- 就是我們熟悉的 48-bit IEEE MAC 地址,如
AA:BB:CC:11:22:33; - 由廠商燒錄,全球唯一(理論上);
- 地址的 最高有效位(MSB)為 0(即“公共地址”標誌);
- 需要向 IEEE(通過 SIG 或直接)購買
- 示例:
D0:CF:5E:xx:xx:xx(很多手機/模塊使用)。
✅ 特點:固定、可識別、無隱私保護。
3. Static Random Address(靜態隨機地址)
- 由設備製造商或開發者設定的一個 隨機生成但永不改變 的地址;
-
必須滿足:
- 最高兩位為
11(表示是靜態隨機地址); - 不能是全 0 或全 1;
- 最高兩位為
- 示例:
DE:AD:BE:EF:CA:FE(只要符合格式且固定即可)。
✅ 特點:固定、不依賴 IEEE 分配、有一定匿名性,但仍可作為身份標識。
📌 很多 IoT 設備(如低成本 BLE 模塊)沒有 Public Address,就用 Static Random Address 作為 Identity Address。
4. 為什麼需要區分?
因為 BLE 隱私機制(RPA)依賴於 Identity Address + IRK 的組合:
- 當設備啓用隱私功能時,它會用 IRK 生成 RPA 來廣播;
- 配對設備收到 RPA 後,用本地存儲的 IRK 嘗試還原出 Identity Address;
- 如果匹配成功,就知道“這是之前配對過的那台設備”。
所以,無論 Identity Address 是 Public 還是 Static Random,只要它是固定的,就能作為“身份錨點”。
💡 實際開發中,你不需要關心它是 Public 還是 Static Random —— 只需知道:這是該設備的唯一身份標識,配對後穩定不變,應該存儲它。
------
## ✅ 總結表
| 概念 | 是否固定 | 是否用於配對 | 是否可用於長期標識 | 備註 |
|---|---|---|---|---|
| Public Address | ✅ 是 | ✅ 是 | ✅ 是 | 傳統 MAC,IEEE 分配 |
| Static Random Address | ✅ 是 | ✅ 是 | ✅ 是 | 設備自定義,高位為 11 |
| Identity Address | ✅ 是 | ✅ 是 | ✅ 是 | = Public 或 Static Random |
| RPA(Resolvable Private Address) | ❌ 否(動態) | ❌ 否 | ❌ 否 | 用於廣播,保護隱私 |
| Non-resolvable Private Address | ❌ 否 | ❌ 否 | ❌ 否 | 完全匿名,通常不可連接 |
------
### 🎯 開發建議
- 不要嘗試解析地址類型,只需在
BOND_BONDED時存儲device.getAddress(); - 這個地址就是系統認可的 Identity Address,無論底層是 Public 還是 Static Random;
- 後續通過
getBondedDevices()匹配該地址即可可靠連接。
四、配對彈窗是怎麼來的?
我們從來沒調 createBond(),為什麼也會彈出系統配對窗口?
後來發現,觸發配對的不是你的代碼,而是 GATT 特徵的安全屬性。
如果你的冰箱聲明某個特徵需要“認證後才能讀寫”(比如設置了 AUTHEN 權限),而當前連接還沒加密,那 Android 在收到 Insufficient Authentication 錯誤後,會自動啓動配對流程。
所以,配對彈窗其實是系統在幫你補安全課。你只需要在配對成功後重試 GATT 操作即可。
五、配對流程
在標準 BLE 通信模型中,配對(Pairing)流程是由 Central(車機)發起的,但實際觸發時機往往由 Peripheral(BLE設備,如冰箱)的安全需求間接驅動
一、協議層面:誰“發起”配對?
根據 Bluetooth Core Specification(BLE 協議規範):
- Central(主設備,如車機) 負責發送
Pairing Request; - Peripheral(從設備,如冰箱) 回覆
Pairing Response; - 後續密鑰交換、確認值計算等均由 Central 主導。
✅ 所以從協議動作看,配對是由 Central(車機)主動發起的。
二、應用層面:誰“觸發”配對?
雖然 Central 發起配對請求,但它通常不是憑空發起,而是因為:
Peripheral 在 GATT 層拒絕了未加密的訪問請求,從而迫使 Central 啓動配對。
典型流程如下:
- 車機(Central)連接冰箱(Peripheral);
- 車機嘗試讀取一個被標記為 “需要認證” 的特徵(例如
read authen); - 冰箱返回錯誤:
Insufficient Authentication (0x05); - Android 系統檢測到該錯誤,自動調用
createBond()併發送Pairing Request; - 配對流程啓動,彈出系統彈窗或走 Just Works 模式。
✅ 所以從觸發原因看,是 Peripheral 的安全策略“迫使” Central 發起配對。
既然是車機發起的配對請求,那車機-Android系統怎麼還能收到配對請求廣播呢?
車機(App)收到的 ACTION_PAIRING_REQUEST 廣播,不是來自遠端設備的“請求”,而是 Android 系統在自己發起配對前,向 App 發出的一個“協商/干預”通知。
這個廣播的目的是:讓 App 有機會干預配對行為
例如:
- 自動確認 Just Works 配對(免彈窗);
- 填入預共享的 PIN 碼;
- 拒絕某些設備的配對。
Pairing 和 Bonding 區別?
這兩個概念經常混用,但職責完全不同:
- Pairing(配對):協商密鑰的過程(生成 LTK、IRK 等),確保本次連接加密;
- Bonding(綁定):把配對成果(密鑰 + 身份)存到本地,供以後複用。
你可以只配對不綁定(每次連都重新確認),但不能只綁定不配對。在 Android 裏,配對成功默認就會綁定,設備進入 getBondedDevices() 列表。
六、怎麼用代碼解綁設備?
Android 沒有公開 removeBond() 方法,但它確實存在,只是被標為 @hide。通過反射調用是行業通用做法:
fun removeBond(device: BluetoothDevice): Boolean {
return try {
val method = device::class.java.getMethod("removeBond")
method.invoke(device) as Boolean
} catch (e: Exception) {
false
}
}
注意:
- 需要
BLUETOOTH_ADMIN權限; - 解綁是異步的,必須監聽
ACTION_BOND_STATE_CHANGED廣播,等狀態變成BOND_NONE; - 最好加兜底:如果反射失敗,引導用户去系統設置手動解綁。
至於為什麼不公開?主要是怕 App 濫用——比如偷偷刪掉用户的耳機配對。但又不能完全禁掉,畢竟車機、IoT 設備確實需要程序化解綁。所以成了“能用,但不鼓勵”的狀態。
總結:幾個關鍵原則
- 地址不變是正常的:那是 Identity Address,不是廣播地址;
- RPA 不需要你關心:系統會自動處理,你只認 bonded 列表;
- 回連必須從
getBondedDevices()取設備,否則 Privacy 一開就斷連; - 配對由 GATT 安全需求驅動,不是靠你調不調
createBond(); - 解綁用反射沒問題,但要做好兼容和用户引導;
- Privacy 是好東西,但前提是兩端都正確實現——設備要真啓用 RPA,車機要會解析。
BLE 看似簡單,但安全和隱私細節很多。理解這套機制,才能做出既好用又安全的產品。希望這篇總結能幫你少走點彎路。