最近新進(jìn)一家公司,主要是做物聯(lián)網(wǎng)這一塊的的,項(xiàng)目需要用到藍(lán)牙開發(fā),講真的,挑戰(zhàn)還是挺大的,做了差不多四年的iOS開發(fā),從沒有接觸過藍(lán)牙開發(fā)這一領(lǐng)域,我是這樣學(xué)習(xí)的。
從網(wǎng)上找各種博客(國內(nèi)的,國外的),借鑒別人寫過的Demo以及官方文檔,花了整整的一周時(shí)間,對iOS的CoreBluetooth這個(gè)框架的使用稍微有一些的了解,請聽我一一道來;
iOS 藍(lán)牙
簡稱:BLE(buletouch low energy),藍(lán)牙 4.0 設(shè)備因?yàn)榈秃碾?,所以也叫?BLE,CoreBluetooth框架就是蘋果公司為我們提供的一個(gè)庫,我們可以使用這個(gè)庫和其他支持藍(lán)牙4.0的設(shè)備進(jìn)行數(shù)據(jù)交互。值得注意的是在IOS10之后的APP中,我們需要在 info.plist文件中添加NSBluetoothPeripheralUsageDescription字段否則APP會崩潰
工作模式:藍(lán)牙通信中,首先需要提到的就是 central 和 peripheral 兩個(gè)概念。這是設(shè)備在通信過程中扮演的兩種角色。直譯過來就是 [中心] 和 [周邊(可以理解為外設(shè))]。iOS 設(shè)備既可以作為 central,也可以作為 peripheral,這主要取決于通信需求。
自己嘗試的寫了個(gè)Demo,實(shí)現(xiàn)的功能有:
1、通過已知外圍設(shè)備的服務(wù)UUID搜索(這個(gè)UUID是指被廣播出來的服務(wù)UUID);
2、連接指定的外圍設(shè)備;
3、獲取指定的服務(wù),發(fā)現(xiàn)需要訂閱的特征;
4、接收外圍設(shè)備發(fā)送的數(shù)據(jù);
5、向外圍設(shè)備寫數(shù)據(jù);
6、實(shí)現(xiàn)藍(lán)牙服務(wù)的后臺模式;
7、實(shí)現(xiàn)藍(lán)牙服務(wù)的狀態(tài)保存與恢復(fù)(應(yīng)用被系統(tǒng)殺死的時(shí)候,系統(tǒng)會自動保存 central manager 的狀態(tài));
中心角色的實(shí)現(xiàn):(central)
(1)、初始化中央管理器對象
/**
第一個(gè)參數(shù):代理
第二個(gè)參數(shù):隊(duì)列(nil為不指定隊(duì)列,默認(rèn)為主隊(duì)列)
第三個(gè)參數(shù):實(shí)現(xiàn)狀態(tài)保存的時(shí)候需要用到 eg:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"}
*/
centerManager = [[CBCentralManager alloc]initWithDelegate:self queue:queue options:options];
中央管理器會調(diào)用 centralManagerDidUpdateState:通知藍(lán)牙的狀態(tài)
(2)、發(fā)現(xiàn)外圍設(shè)備
[centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:SERVICE_UUID]] options:nil];
每次中央管理器發(fā)現(xiàn)外圍設(shè)備時(shí),它都會調(diào)用centralManager:didDiscoverPeripheral:advertisementData:RSSI:其委托對象的方法。
(3)、發(fā)現(xiàn)想要的外圍設(shè)備進(jìn)行連接
#pragma mark -- 掃描發(fā)現(xiàn)到任何一臺設(shè)備都會通過這個(gè)代理方法回調(diào)
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
{
//過濾掉無效的結(jié)果
if (peripheral == nil||peripheral.identifier == nil/*||peripheral.name == nil*/)
{
return;
}
NSString *pername =[NSString stringWithFormat:@"%@",peripheral.name];
NSLog(@"所有服務(wù)****:%@",peripheral.services);
NSLog(@"藍(lán)牙名字:%@ 信號強(qiáng)弱:%@",pername,RSSI);
//連接需要的外圍設(shè)備
[self connectPeripheral:peripheral];
//將搜索到的設(shè)備添加到列表中
[self.peripherals addObject:peripheral];
if (_didDiscoverPeripheralBlock) {
_didDiscoverPeripheralBlock(central,peripheral,advertisementData,RSSI);
}
}
如果連接請求成功,則中央管理器調(diào)用centralManager:didConnectPeripheral:其委托對象的方法。
(4)、發(fā)現(xiàn)所連接的外圍設(shè)備的服務(wù)
#pragma mark -- 連接成功、獲取當(dāng)前設(shè)備的服務(wù)和特征 并停止掃描
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
NSLog(@"%@",peripheral);
// 設(shè)置設(shè)備代理
[peripheral setDelegate:self];
// 大概獲取服務(wù)和特征
[peripheral discoverServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
NSLog(@"Peripheral Connected");
if (_centerManager.isScanning) {
[_centerManager stopScan];
}
NSLog(@"Scanning stopped");
}
發(fā)現(xiàn)指定的服務(wù)時(shí),外圍設(shè)備(CBPeripheral你連接的對象)會調(diào)用peripheral:didDiscoverServices:其委托對象的方法。
(5)、發(fā)現(xiàn)服務(wù)的特征
#pragma mark -- 獲取當(dāng)前設(shè)備服務(wù)services
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
if (error) {
NSLog(@"Error discovering services: %@", [error localizedDescription]);
return;
}
NSLog(@"所有的servicesUUID%@",peripheral.services);
//遍歷所有service
for (CBService *service in peripheral.services)
{
NSLog(@"服務(wù)%@",service.UUID);
//找到你需要的servicesuuid
if ([[NSString stringWithFormat:@"%@",service.UUID] isEqualToString:SERVICE_UUID])
{
// 根據(jù)UUID尋找服務(wù)中的特征
[peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:CHARACTERISTIC_UUID]] forService:service];
}
}
}
peripheral:didDiscoverCharacteristicsForService:error:當(dāng)發(fā)現(xiàn)指定服務(wù)的特征時(shí),外圍設(shè)備調(diào)用其委托對象的方法。
(6)、檢索特征價(jià)值
閱讀特征的值 ()
[peripheral readValueForCharacteristic:interestingCharacteristic];
注意: 并非所有特征都是可讀的。你可以通過檢查其properties屬性是否包含CBCharacteristicPropertyRead常量來確定特征是否可讀。如果嘗試讀取不可讀的特征值,則peripheral:didUpdateValueForCharacteristic:error:委托方法將返回合適的錯(cuò)誤。
訂閱特征的值()
雖然使用該readValueForCharacteristic:方法讀取特征值對靜態(tài)值有效,但它不是檢索動態(tài)值的最有效方法。檢索隨時(shí)間變化的特征值 - 例如,你的心率 - 通過訂閱它們。訂閱特征值時(shí),您會在值更改時(shí)收到外圍設(shè)備的通知。
[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];
注意: 并非所有特征都提供訂閱。你可以通過檢查特性是否properties包含其中一個(gè)CBCharacteristicPropertyNotify或多個(gè)CBCharacteristicPropertyIndicate常量來確定特征是否提供訂閱。
當(dāng)你訂閱(或取消訂閱)特征的值時(shí),外圍設(shè)備會調(diào)用peripheral:didUpdateNotificationStateForCharacteristic:error:其委托對象的方法。
寫一個(gè)特征的值 ()
有時(shí)寫一個(gè)特征的值是有意義的。例如,如果你的應(yīng)用程序與藍(lán)牙低功耗數(shù)字恒溫器交互,你可能需要為恒溫器提供設(shè)置房間溫度的值。如果特征值是可寫的,則可以NSData通過調(diào)用外設(shè)writeValue:forCharacteristic:type:方法將數(shù)據(jù)值;
[self.discoveredPeripheral writeValue:data forCharacteristic:self.characteristic1 type:CBCharacteristicWriteWithResponse];
寫入特征的值時(shí),指定要執(zhí)行的寫入類型。在上面的示例中,寫入類型CBCharacteristicWriteWithResponse指示外圍設(shè)備通過調(diào)用peripheral:didWriteValueForCharacteristic:error:其委托對象的方法讓您的應(yīng)用程序知道寫入是否成功。
外圍角色的實(shí)現(xiàn)
(1)、初始化外圍設(shè)備管理器
peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
創(chuàng)建外圍設(shè)備管理器時(shí),外圍設(shè)備管理器會調(diào)用peripheralManagerDidUpdateState:其委托對象的方法。您必須實(shí)現(xiàn)此委托方法,以確保支持藍(lán)牙低功耗并可在本地外圍設(shè)備上使用。
(2)、設(shè)置服務(wù)和特征
為自定義服務(wù)和特征創(chuàng)建自己的UUID
在終端使用 uuidgen 命令獲取以ASCII字符串形式的128位值的UUID:71DA3FD1-7E10-41C1-B16F-4430B506CDE7
構(gòu)建服務(wù)樹和特征
myCharacteristic =[[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead value:myValue permissions:CBAttributePermissionsReadable]; //特征
myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES]; //與特征所關(guān)聯(lián)的服務(wù)
myService.characteristics = @ [myCharacteristic]; //設(shè)置服務(wù)的特征數(shù)組,將特征與其關(guān)聯(lián)
(3)、發(fā)布服務(wù)和特征
[peripheralManager addService:myService];
當(dāng)調(diào)用此方法發(fā)布服務(wù)時(shí),外圍管理器將調(diào)用peripheralManager:didAddService:error:其委托對象的方法。通過error可以知道是否發(fā)布成功;
將服務(wù)及其任何關(guān)聯(lián)特性發(fā)布到外圍設(shè)備的數(shù)據(jù)庫后,該服務(wù)將被緩存,將無法再對其進(jìn)行更改。
(4)、廣播服務(wù)
[peripheralManager startAdvertising:@ {CBAdvertisementDataServiceUUIDsKey:@[myFirstService.UUID,mySecondService.UUID]}];
當(dāng)開始在本地外圍設(shè)備上公布某些數(shù)據(jù)時(shí),外圍設(shè)備管理器會調(diào)用peripheralManagerDidStartAdvertising:error:其委托對象的方法。
(5)、響應(yīng)來自中央的讀取和寫入請求
當(dāng)連接的中央請求讀取某個(gè)特征的值時(shí),外圍管理器會調(diào)用peripheralManager:didReceiveReadRequest:其委托對象的方法。
[peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
設(shè)置讀取請求不要求從超出特征值的邊界的索引位置讀取
request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset,myCharacteristic.value.length - request.offset)];
將請求的特性屬性(默認(rèn)值為nil)的值設(shè)置為您在本地外圍設(shè)備上創(chuàng)建的特征值,同時(shí)考慮讀取請求的偏移量
設(shè)置值后,響應(yīng)遠(yuǎn)程中央以指示請求已成功完成。通過調(diào)用類的respondToRequest:withResult:方法CBPeripheralManager,傳回請求(其更新的值)和請求的結(jié)果
當(dāng)連接的中心發(fā)送寫入一個(gè)或多個(gè)特征值的請求時(shí),外圍管理器會調(diào)用peripheralManager:didReceiveWriteRequests:其委托對象的方法
(6)、將更新的特征值發(fā)送到訂閱的中心
當(dāng)連接的中心訂閱某個(gè)特征的值時(shí),外圍管理器會調(diào)用peripheralManager:central:didSubscribeToCharacteristic:其委托對象的方法
獲取特征的更新值,并通過調(diào)用類的updateValue:forCharacteristic:onSubscribedCentrals:方法將其發(fā)送到中心CBPeripheralManager。
處理常駐后臺任務(wù)
首先需要在Capabilities-->Background Modes申請中心角色的后臺模式說明
如圖:

(1)、狀態(tài)保存與恢復(fù)
因?yàn)闋顟B(tài)的保存和恢復(fù) Core Bluetooth 都為我們封裝好了,所以我們只需要選擇是否需要這個(gè)特性即可。系統(tǒng)會保存當(dāng)前 central manager 或 peripheral manager,并且繼續(xù)執(zhí)行藍(lán)牙相關(guān)事件(即使程序已經(jīng)不再運(yùn)行)。一旦事件執(zhí)行完畢,系統(tǒng)會在后臺重啟 app,這時(shí)你有機(jī)會去存儲當(dāng)前狀態(tài),并且處理一些事物。在之前提到的 “門鎖” 的例子中,系統(tǒng)會監(jiān)視連接請求,并在 centralManager:didConnectPeripheral: 回調(diào)時(shí),重啟 app,在用戶回家后,連接操作結(jié)束。
Core Bluetooth 的狀態(tài)保存與恢復(fù)在設(shè)備作為 central、peripheral 或者這兩種角色時(shí),都可用。在設(shè)備作為 central 并添加了狀態(tài)保存與恢復(fù)支持后,如果 app 被強(qiáng)行關(guān)閉進(jìn)程,系統(tǒng)會自動保存 central manager 的狀態(tài)(如果 app 有多個(gè) central manager,你可以選擇哪一個(gè)需要系統(tǒng)保存)。
對于 CBCentralManager,系統(tǒng)會保存以下信息:
central 準(zhǔn)備連接或已經(jīng)連接的 peripheral
central 需要掃描的 service(包括掃描時(shí),配置的 options)
central 訂閱的 characteristic
對于 peripheral 來說,情況也差不多。系統(tǒng)對 CBPeripheralManager 的處理方式如下:
peripheral 在廣播的數(shù)據(jù)
peripheral 存入的 service 和 characteristic 的樹形結(jié)構(gòu)
已經(jīng)被 central 訂閱了的 characteristic 的值
當(dāng)系統(tǒng)在后臺重新加載程序后(可能是因?yàn)檎业搅艘业?peripheral),你可以重新實(shí)例化 central manager 或 peripheral 并恢復(fù)他們的狀態(tài)。
(2)、選擇支持存儲和恢復(fù)
如果要支持存儲和恢復(fù),則需要在初始化 manager 的時(shí)候給一個(gè) restoration identifier。restoration identifier 是 string 類型,并標(biāo)識了 app 中的 central manager 或 peripheral manager。這個(gè) string 很重要,它將會告訴 Core Bluetooth 需要存儲狀態(tài),畢竟 Core Bluetooth 恢復(fù)有 identifier 的對象。
例如,在 central 端,要想支持該特性,可以在調(diào)用 CBCentralManager 的初始化方法時(shí),配置 CBCentralManagerOptionRestoreIdentifierKey:
centralManager = [[CBCentralManager alloc] initWithDelegate:self
queue:nil
options:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"}];
雖然以上代碼沒有展示出來,其實(shí)在 peripheral manager 中要設(shè)置 identifier 也是這樣的。只是在初始化時(shí),將 key 改成了 CBPeripheralManagerOptionRestoreIdentifierKey。
因?yàn)槌绦蚩梢杂卸鄠€(gè) CBCentralManager 和 CBPeripheralManager,所以要確保每個(gè) identifier 都是唯一的。
(3)、重新初始化 central manager 和 peripheral manager
當(dāng)系統(tǒng)重新在后臺加載程序時(shí),首先需要做的即根據(jù)存儲的 identifier,重新初始化 central manager 或 peripheral manager。如果你只有一個(gè) manager,并且 manager 存在于 app 生命周期中,那這個(gè)步驟就不需要做什么了。
.
如果 app 中包含多個(gè) manager,或者 manager 不是在整個(gè) app 生命周期中都存在的,那 app 就必須要區(qū)分你要重新初始化哪個(gè) manager 了。你可以通過從 app delegate 中的 application:didFinishLaunchingWithOptions: 中取出 key(UIApplicationLaunchOptionsBluetoothCentralsKey 或 UIApplicationLaunchOptionsBluetoothPeripheralsKey)中的 value(數(shù)組類型)來得到程序退出之前存儲的 manager identifier 列表:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSArray *centralManagerIdentifiers =
launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
if (centralManagerIdentifiers.count) {
//重新初始化所有的 manager
for (NSString *identifier in centralManagerIdentifiers) {
NSLog(@"系統(tǒng)啟動項(xiàng)目");
//在這里創(chuàng)建的藍(lán)牙實(shí)例一定要被當(dāng)前類持有,不然出了這個(gè)函數(shù)就被銷毀了,藍(lán)牙檢測會出現(xiàn)“XPC connection invalid”
self.bluetooth = [[MSBBlueTooth alloc]initWithQueue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey : identifier}];
NSLog(@"");
}
}
return YES;
}
(4)、實(shí)現(xiàn)恢復(fù)狀態(tài)的代理方法
在重新初始化 manager 之后,接下來需要同步 Core Bluetooth 存儲的他們的狀態(tài)。要想弄清楚在程序被退出時(shí)都在做些什么,就需要正確的實(shí)現(xiàn)代理方法。對于 central manager 來說,需要實(shí)現(xiàn) centralManager:willRestoreState:;對于 peripheral manager 來說,需要實(shí)現(xiàn) peripheralManager:willRestoreState:。
.
注意:如果選擇存儲和恢復(fù)狀態(tài),當(dāng)系統(tǒng)在后臺重新加載程序時(shí),首先調(diào)用的方法是 centralManager:willRestoreState: 或 peripheralManager:willRestoreState:。如果沒有選擇存儲的恢復(fù)狀態(tài)(或者喚醒時(shí)沒有什么內(nèi)容需要恢復(fù)),那么首先調(diào)用的方法是 centralManagerDidUpdateState: 或 peripheralManagerDidUpdateState:。
.
無論是以上哪種代理方法,最后一個(gè)參數(shù)都是一個(gè)包含程序退出前狀態(tài)的字典。字典中,可用的 key ,
central 端有:
NSString *const CBCentralManagerRestoredStatePeripheralsKey;
NSString *const CBCentralManagerRestoredStateScanServicesKey;
NSString *const CBCentralManagerRestoredStateScanOptionsKey;
peripheral 端有:
NSString *const CBPeripheralManagerRestoredStateServicesKey;
NSString *const CBPeripheralManagerRestoredStateAdvertisementDataKey;
要恢復(fù) central manager 的狀態(tài),可以用 centralManager:willRestoreState: 返回字典中的 key 來得到。假如說 central manager 有想要或者已經(jīng)連接的 peripheral,那么可以通過 CBCentralManagerRestoredStatePeripheralsKey 對應(yīng)得到的 peripheral(CBPeripheral 對象)數(shù)組來得到。
- (void)centralManager:(CBCentralManager *)central
willRestoreState:(NSDictionary *)state {
NSArray *peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey];
//講狀態(tài)保存的設(shè)備加入列表,在藍(lán)牙檢測狀態(tài)的回調(diào)里實(shí)現(xiàn)重連
self.peripherals = [NSMutableArray arrayWithArray:peripherals];
}
具體要對拿到的 peripheral 數(shù)組做什么就要根據(jù)需求來了。如果這是個(gè) central manager 搜索到的 peripheral 數(shù)組,那就可以存儲這個(gè)數(shù)組的引用,并且開始建立連接了(注意給這些 peripheral 設(shè)置代理,否則連接后不會走 peripheral 的代理方法)。
.
恢復(fù) peripheral manager 的狀態(tài)和 central manager 的方式類似,就只是把代理方法換成了 peripheralManager:willRestoreState:,并且使用對應(yīng)的 key 即可
寫的不是很好,也算是東拼西湊了,但也是花了時(shí)間去整理的,如果看不懂,可以下載我的Demo自己跑一遍;
想要看實(shí)現(xiàn)效果,可以下載Demo,看的再多也不如項(xiàng)目跑一遍來的快,療效是不騙人的;
有需要的可以加我微信Jarvis-LLL,一起討論學(xué)習(xí)
喜歡就點(diǎn)個(gè)贊,也可以在下方評論一起討論討論