最近公司的項目中恰好用到了 CoreBluetooth 相關的知識,在看了網上不少文章及官方文檔的介紹後,如今對大概的框架有了基本的瞭解。html
在此,寫下這篇文章記錄本身這段時間的成果。文章的最後會有一個小Demo,簡單的實現了一些基本的功能,供各位看官把玩。Demo的代碼能夠去個人Github中下載。git
這篇文章主要以實踐的方式,實現了藍牙交互中的兩個角色。關於基本概念的介紹,官方文檔中介紹的很是詳細,你們能夠仔細研究一下。github
CoreBluetooth 中最關鍵的兩個角色就是 Central(中心) 和 Peripheral(周邊)。數組
Central 在鏈接中做爲主動發起者會去尋找待鏈接的 Peripheral。緩存
Peripheral 通常是提供服務的一方, Central 獲取 Peripheral 提供的服務而後來完成特定的任務。bash
Peripheral 經過向空中廣播數據的方式來使咱們能感知到它的存在。Central 經過掃描搜索來發現周圍正在廣播數據的 Peripheral, 找到指定的 Peripheral 後,發送鏈接請求進行鏈接,鏈接成功後則與 Peripheral 進行一些數據交互, Peripheral 則會經過合適的方式對 Central 進行響應。app
這是在開發中最多見的一個需求,須要自身做爲 Central 的對象,去發現一些外設的服務,並根據其提供的數據進行相應的操做。例如,鏈接一個小米手環,根據其提供的步數、心率等信息調整UI的顯示。框架
實現步驟:學習
centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey : true])
複製代碼
CBCentralManagerOptionShowPowerAlertKey: 當藍牙狀態爲 powered off時,系統會彈出提示框。優化
CBCentralManagerOptionRestoreIdentifierKey:生成一個惟一的標識符,用於後續應用恢復這個manager。
當建立了 CentralManager 以後, CBCentralManagerDelegate 會經過下面的回調告知你,當前設備是否支持你去使用藍牙的功能。
func centralManagerDidUpdateState(_ central: CBCentralManager)
複製代碼
經過 central.state 能夠獲取當前的狀態
case unknown //未知狀態
case resetting // 鏈接斷開,即將重置
case unsupported // 該設備不支持藍牙
case unauthorized // 藍牙未受權
case poweredOff // 藍牙關閉
case poweredOn // 藍牙正常開啓
複製代碼
只有當藍牙的狀態爲正常開啓的狀態時,才能進行後續的步驟。
self.centralManager?.scanForPeripherals(withServices: [CBUUID(string: UUID_SERVICE)], options: [CBCentralManagerOptionShowPowerAlertKey : true])
複製代碼
UUID_SERVICE 一般是由Central及Peripheral本身定義的,兩端使用同一個UUID。
每當 CentralManager 搜索到一個 Peripheral 設備時,就會經過代理方法進行回調。若是你後面須要鏈接這個 Peripheral,須要定義一個 CBPeripheral 類型的對象來指向(強引用)這個對象,這樣系統暫時就不會釋放這個對象了。
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber)
複製代碼
當發現自身須要的 Peripheral 時,爲了減小藍牙對電量的消耗,能夠中止CentralManager的掃描。 self.centralManager?.stopScan()
self.centralManager?.connect(peripheral, options: nil)
複製代碼
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
複製代碼
當成功鏈接 Peripheral 以後,咱們須要去查找 Peripheral 爲咱們提供的服務。爲了能收到 Peripheral 查找的結果,咱們須要去遵照 Peripheral 對象的代理方法 CBPeripheralDelegate。
self.configPeripheral?.delegate = self
複製代碼
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?)
複製代碼
self.configPeripheral?.discoverServices([CBUUID(string: UUID_SERVICE)])
複製代碼
這裏咱們能夠傳入一個關於 Service 的一個數組,查找本身須要的 Service。固然也能夠傳入 nil,這樣就會查找到 Peripheral 提供的所有 Service。
可是,通常來講,爲了節省電量以及一些沒必要要的時間浪費,會傳入本身須要的 Service 的數組。
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?)
複製代碼
CBPeripheralDelegate 的 搜索 Service 回調。在這裏出現錯誤以後,能夠進行重試或者拋出中止鏈接流程。
若成功的話,則能夠繼續搜索對應 Service 的 Characteristic。
peripheral.discoverCharacteristics([CBUUID(string: UUID_READABLE), CBUUID(string: UUID_WRITEABLE)], for: service)
複製代碼
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
複製代碼
CBPeripheralDelegate 的 搜索 Characteristic 的回調。若 Characteristic 有多個,則此方法會回調屢次。
peripheral.readValue(for: characteristic)
複製代碼
當你嘗試去讀取一個 Characteristic 的值時, Peripheral 會經過下面的代理回調來返回結果,你能夠經過 Characteristic 的 value 屬性來獲得這個值。
並非全部的 Characteristic 的值都是可讀的,決定一個 Characteristic 的值是否可讀是經過檢查 Characteristic 的 Properties 屬性是否包含 CBCharacteristicPropertyRead 常量來判斷的。當你嘗試去讀取一個值不可讀的 Characteristic 時,下面的代理方法會返回一個Error供你處理。
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
複製代碼
當一個咱們須要讀取 Characteristic 的值,頻繁變化時,read 操做就會顯得很繁瑣。這個時候咱們就能夠經過訂閱的方式獲取值得更新。若是訂閱了某個 Characteristic 以後,每當值有變化時,也會經過 peripheral(_ peripheral:, didUpdateValueFor characteristic:, error:) 方法回調使咱們收到每次更新的值。
peripheral.setNotifyValue(true, for: characteristic)
複製代碼
當咱們訂閱了一個 Characteristic 後,CBPeripheralDelegate 會經過下面的回調,使咱們知道訂閱的 Characteristic 的狀態。
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?)
複製代碼
並非全部的 Characteristic 都提供訂閱功能,決定一個 Characteristic 是否能訂閱是經過檢查 Characteristic 的 properties 屬性是否包含 CBCharacteristicPropertyNotify 或者 CBCharacteristicPropertyIndicate 常量來判斷的。
決定 Characteristic 的值是否可寫,須要經過查看 Characteristic 的 properties 屬性是否包含 CBCharacteristicPropertyWriteWithoutResponse 或者 CBCharacteristicPropertyWrite 常量來判斷的。
peripheral.writeValue(data, for: characteristic, type: .withResponse)
複製代碼
type: 寫入類型。
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?)
複製代碼
將自身聲明爲一個 Peripheral,可爲 Central 對象提供一些服務。例如:在A手機利用鏈接B手機時,B手機此時就是 Peripheral 對象。
實現步驟:
myPeripheral = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey : true])
複製代碼
當建立了 Peripheral 以後, CBPeripheralManagerDelegate 會經過下面的回調告知你,當前設備是否支持你去使用藍牙的功能。
狀態同 CentralManager 同樣的。
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager)
複製代碼
只有當藍牙的狀態爲正常開啓的狀態時,才能進行後續的步驟。
let characteristic_read = CBUUID(string: UUID_READABLE)
let characteristic_write = CBUUID(string: UUID_WRITEABLE)
let serviceUUID = CBUUID(string: UUID_SERVICE)
// 爲服務指定一個特徵 讀取特徵 可被訂閱
myCharacteristic_beRead = CBMutableCharacteristic(type: characteristic_read,
properties: [.read, .notify],
value: nil,
permissions: .readable)
myCharacteristic_beWrite = CBMutableCharacteristic(type: characteristic_write,
properties: .write,
value: nil,
permissions: .writeable)
// 建立一個服務
myService = CBMutableService(type: serviceUUID, primary: true)
複製代碼
上面的代碼中,咱們建立了兩個 Characteristic,一個可讀且可被訂閱,另外一個可寫。這個是經過實例化時,傳入的 properties 及 permissions 的值來指定的。
這裏須要注意的是 value 傳入的值是 nil。由於,若是你指定了 Characteristic 的值,那麼該值將被緩存而且該 Characteristic 的 properties 和 permissions 將被設置爲可讀的。所以,若是你須要 Characteristic 的值是可寫的,或者你但願在 Service 發佈後,Characteristic 的值在 lifetime(生命週期)中依然能夠更改,你必須將該 Characteristic 的值指定爲 nil。經過這種方式能夠確保 Characteristic 的值,在 PeripheralManager 收到來自鏈接的 Central 的讀或者寫請求的時候,可以被動態處理。
// 將特徵加入到服務中
myService!.characteristics = ([myCharacteristic_beRead, myCharacteristic_beWrite] as! [CBCharacteristic])
// 將服務加入到外設中
myPeripheral?.add(myService!)
複製代碼
這裏咱們將本身的服務構建完成,並將服務添加至 Peripheral 中。這樣,當 Peripheral 向外界發送廣播時,就能夠搜索這個服務獲取相應的支持。
// 廣播本身的service
myPeripheral?.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [myService!.uuid], CBAdvertisementDataLocalNameKey: "我建立了一個房間"])
複製代碼
在廣播時,能夠同時攜帶一些數據。這裏能夠傳入一個字典,可是這個字典只支持傳入 CBAdvertisementDataLocalNameKey 及 CBAdvertisementDataServiceUUIDsKey。
關於廣播方法官方文檔的說明以下:
When in the foreground, an application can utilize up to 28 bytes of space in the initial advertisement data for any combination of the supported advertising data types. If this space is used up, there are an additional 10 bytes of space in the scan response that can be used only for the local name. Note that these sizes do not include the 2 bytes of header information that are required for each new data type. Any service UUIDs that do not fit in the allotted space will be added to a special "overflow" area, and can only be discovered by an iOS device that is explicitly scanning for them.
While an application is in the background, the local name will not be used and all service UUIDs will be placed in the "overflow" area. However, applications that have not specified the "bluetooth-peripheral" background mode will not be able to advertise anything while in the background.
大概意思是:
當處於前臺時,應用程序能夠在初始廣告數據中利用28個字節的空間用來初始化廣播數據字典,該字典包含兩個支持的 key。若是此空間已用完,掃描響應時最後還會添加10個字節的空間,只能用於Local Name。
請注意,這些大小不包括每種新數據類型所需的2個字節的頭部信息。任何不適合分配空間的服務UUID都將添加到特殊的「溢出」區域,而且只能由明確掃描它們的iOS設備發現。
當應用程序在後臺時,將不使用本地名稱,而且全部服務UUID將放置在「溢出」區域中。可是,未指定「藍牙後臺運行」背景模式的應用程序將沒法在後臺播聽任何內容。
/// 收到來自中心設備讀取數據的請求
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest)
/// 收到來自中心設備寫入數據的請求
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest])
複製代碼
當 Peripheral 收到來自 Central 的讀寫請求時,CBPeripheralManagerDelegate 會經過上面兩個方法來進行回調。讀寫請求以 CBATTRequest 對象來傳遞。
當咱們收到請求時,能夠根據 CBATTRequest 請求的一些屬性來判斷 Central 指定要讀寫的 Characteristic 是否和設備服務庫中的 Characteristic 是否相匹配。
if request.characteristic.uuid.isEqual(CBUUID(string: UUID_READABLE)) {
// do something
myPeripheral?.respond(to: request, withResult: CBATTError.Code.success)
} else {
// not match
myPeripheral?.respond(to: request, withResult: CBATTError.Code.readNotPermitted)
}
複製代碼
最後使用 respond(to:, withResult:) 迴應 Central 請求。
這裏的 result 是一個 CBATTError.Code 類型,這裏定義了不少對 Request 響應的枚舉,能夠根據對 Request 的響應,返回相應的 CBATTError.Code 值。
利用藍牙鏈接兩個設備,進行一場激情的五子棋小遊戲吧。
示例工程能夠去個人Giuhub下載。
目前工程內完成了基本的兩端交互邏輯。對於掉線重連等優化問題目前示例工程內還沒有體現。
建立房間: 實現 Local Peripheral 端功能。將本身建立了房間的消息進行廣播,使其它玩家能夠掃描到房間進入遊戲。並利用一個可被訂閱的 Characteristic 將消息傳輸給 Central,一個可寫的 Characteristic 接收來自 Central 的消息。
尋找房間: 實現 Local Central 端功能。能夠掃描其它玩家建立的房間並加入遊戲。經過可寫的 Characteristic 將消息傳輸給 Peripheral,訂閱一個 Characteristic 來獲取值更新的通知。