藍(lán)牙基礎(chǔ)
IOS中關(guān)于藍(lán)牙的框架其實(shí)有四個(gè):
(1)GameKit.framework 根據(jù)名稱我們可以猜出,這是個(gè)游戲開發(fā)API,僅限于ios設(shè)備之間的連接。
(2)MultipeerConnectivity.framework iOS7將GameKit中的藍(lán)牙模塊單獨(dú)出的一個(gè)Multipeer Connectivity Framework,通過發(fā)現(xiàn)附近的設(shè)備用wifi或藍(lán)牙進(jìn)行p2p連接,限ios設(shè)備之間互相傳文件用的。
(3)ExternalAccessory.framework 用于和第三方藍(lán)牙進(jìn)行交互,必須是MFI認(rèn)證的設(shè)備。
(4)CoreBluetooth.framework 這就是我們的要細(xì)細(xì)研究的了,主要用于和第三方藍(lán)牙的交互,必須是藍(lán)牙4.0以上的設(shè)備,藍(lán)牙4.0也叫BLE(Bluetooth Low Energy)所以一般都稱之為BlE開發(fā),從iPhone4s及其以后的設(shè)備都是支持BLE的。
藍(lán)牙開發(fā)分為中心者模式和管理者模式. 我們絕大多數(shù)App使用的都是中心者模式,這次先來講一下在中心者模式開發(fā)基本流程.
1.創(chuàng)建中心設(shè)備管理器
// 創(chuàng)建中心設(shè)備管理器,會(huì)回調(diào)centralManagerDidUpdateState
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
//藍(lán)牙power沒打開時(shí)alert提示框 iOS11設(shè)置頁里關(guān)閉才會(huì)彈
[NSNumber numberWithBool:YES],CBCentralManagerOptionShowPowerAlertKey, @"amigoCentralManagerIdentifier",CBCentralManagerOptionRestoreIdentifierKey,nil];
NSArray *backgroundModes = [[[NSBundle mainBundle] infoDictionary]objectForKey:@"UIBackgroundModes"];
if ([backgroundModes containsObject:@"bluetooth-central"]) {
//info.plist 有聲明藍(lán)牙使用 后臺(tái)模式
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:options];
}
else {
//非后臺(tái)模式
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
}
2.獲取到藍(lán)牙狀態(tài)
//只要中心管理者初始化 就會(huì)觸發(fā)此代理方法 判斷手機(jī)藍(lán)牙狀態(tài)
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
NSLog(@"檢測(cè)到當(dāng)前藍(lán)牙狀態(tài)::%ld",central.state);
if (self.stateBlock) {
self.stateBlock(central.state);
}
}
3.掃描外設(shè)
// 根據(jù)SERVICE_UUID來掃描外設(shè),如果不設(shè)置SERVICE_UUID,則掃描所有藍(lán)牙設(shè)備 正常業(yè)務(wù)我們只識(shí)別自己的服務(wù)廠商的UUID
[self.centralManager.defaultCentralManager scanForPeripheralsWithServices:self.serviceUUIDs options:nil];
4.發(fā)現(xiàn)服務(wù),掃描指定外設(shè)的特征值
/** 發(fā)現(xiàn)符合要求的外設(shè),回調(diào) */
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI {
//連接指定的設(shè)備 通過匹配設(shè)備名稱或其他方式
NSString *macKey = [advertisementData objectForKey:@"kCBAdvDataLocalName"];
if (!ISEmptyString(macKey) && [self.deviceName isEqualToString:macKey]) {
NSLog(@"掃描到目標(biāo)外設(shè) 準(zhǔn)備連接:::%ld::UUIDString:%@",peripheral.state,[peripheral identifier].UUIDString);
if (peripheral.state == CBPeripheralStateConnecting) {
self.connectState = XLBluetoothConnectStateConnectFailed;
if ([_delegate respondsToSelector:@selector(centralManagerBluetoothConnectState:)]) {
[_delegate centralManagerBluetoothConnectState:XLBluetoothConnectStateConnectFailed];
}
}
else
{
self.peripheral = peripheral;
self.connectState = XLBluetoothConnectStateConnecting;
if ([_delegate respondsToSelector:@selector(centralManagerBluetoothConnectState:)]) {
[_delegate centralManagerBluetoothConnectState:XLBluetoothConnectStateConnecting];
}
//連接外設(shè)
[self.centralManager.defaultCentralManager connectPeripheral:peripheral options:nil];
}
}
}
5.連接外設(shè)成功,尋找服務(wù)
/** 連接成功 */
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
// 可以停止掃描
NSLog(@"連接成功 停止掃描");
[self.centralManager.defaultCentralManager stopScan];
self.connectState = XLBluetoothConnectStateConnectSuccessed;
if ([_delegate respondsToSelector:@selector(centralManagerBluetoothConnectState:)]) {
[_delegate centralManagerBluetoothConnectState:XLBluetoothConnectStateConnectSuccessed];
}
// 設(shè)置代理
_peripheral.delegate = self;
// 根據(jù)UUID來尋找服務(wù)
[_peripheral discoverServices:self.serviceUUIDs];
}
6.發(fā)現(xiàn)服務(wù)
/** 發(fā)現(xiàn)服務(wù) */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
// 遍歷出外設(shè)中所有的服務(wù)
for (CBService *service in peripheral.services) {
NSLog(@"所有的服務(wù):%@",service);
// 根據(jù)UUID尋找服務(wù)中的特征 連接某個(gè)設(shè)備 serviceUUID 就只能是單個(gè)的
if ([service.UUID isEqual:self.serviceUUIDs.firstObject]) {
// characteristicUUIDs : 可以指定想要掃描的特征(傳nil,掃描所有的特征)
[peripheral discoverCharacteristics:nil forService:service];
}
}
}
7.發(fā)現(xiàn)特征回調(diào)
/** 發(fā)現(xiàn)特征回調(diào) */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
NSLog(@"service.characteristics::%ld",service.characteristics.count);
// 遍歷出所需要的特征
for (CBCharacteristic *characteristic in service.characteristics) {
if (characteristic.properties & CBCharacteristicPropertyRead) {
// 直接讀取這個(gè)特征數(shù)據(jù),會(huì)調(diào)用didUpdateValueForCharacteristic
[peripheral readValueForCharacteristic:characteristic];
}
if ((characteristic.properties & CBCharacteristicPropertyNotify) || (characteristic.properties & CBCharacteristicPropertyIndicate)) {
// 訂閱通知
self.notifCharacteristic = characteristic;
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
}
if (characteristic.properties & CBCharacteristicPropertyWriteWithoutResponse) {
NSLog(@"Properties is Write");
self.writeCharacteristic = characteristic;
//必須等notifCharacteristic 注冊(cè)了之后才能去寫數(shù)據(jù) 否則數(shù)據(jù)結(jié)果會(huì)沒有回調(diào)
if (!ISEmptyString(self.command) && _notifCharacteristic) {
[self writeCommandToDevice:self.command];
}
// [peripheral discoverDescriptorsForCharacteristic:characteristic];
}
NSLog(@"the property :%lu",(unsigned long)characteristic.properties );
}
}
8.訂閱狀態(tài)改變,可以開始寫數(shù)據(jù)
/** 訂閱狀態(tài)的改變 */
-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if (error == nil) {
if (characteristic.isNotifying) {
NSLog(@"訂閱成功");
if ([self.delegate respondsToSelector:@selector(didUpdateNotificationStateSuccess)]) {
[self.delegate didUpdateNotificationStateSuccess];
}
/** 如果有命令未下發(fā)的,訂閱成功可以開始寫數(shù)據(jù)了**/
if (!ISEmptyString(self.command)) {
[self writeCommandToDevice:self.command];
}
}
}
}
9.接收藍(lán)牙數(shù)據(jù)
/** 接收到數(shù)據(jù)回調(diào) */
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
// 拿到外設(shè)發(fā)送過來的數(shù)據(jù) 只接受指定notif的特征值
if (![characteristic isEqual:_notifCharacteristic]) {
return;
}
if (error == nil) {
//藍(lán)牙回復(fù)的內(nèi)容 16進(jìn)制的Data 需要轉(zhuǎn)成String
NSData *data = characteristic.value;
NSString *content = [NSString convertDataToHexStr:data];
if ([_delegate respondsToSelector:@selector(peripheralReportContent:)]) {
[_delegate peripheralReportContent:content];
}
NSLog(@"didUpdateValueForCharacteristic ::%@",content);
}
}
10 藍(lán)牙斷開連接
//主動(dòng)斷開藍(lán)牙連接
[self.centralManager cancelPeripheralConnection:_peripheral];
/** 斷開連接 回調(diào) */
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error {
NSLog(@"斷開連接");
}
藍(lán)牙中心者模式開發(fā)的基本流程就是這樣的了,這個(gè)過程藍(lán)牙數(shù)據(jù)傳輸?shù)母袷绞鞘M(jìn)制的NSData,發(fā)送一般20字節(jié)一次(這個(gè)是由BLE的MTU決定的),如果想要傳輸更多字節(jié)數(shù),可以采用分包等方式,一般的藍(lán)牙功能20字節(jié)也是夠用了,具體的傳輸協(xié)議需要和藍(lán)牙的硬件開發(fā)商協(xié)調(diào)溝通,一般都會(huì)有個(gè)說明書.
現(xiàn)在說一下,在整個(gè)藍(lán)牙開發(fā)過程中遇到過的兩個(gè)問題:
1.CBCharacteristicWriteWithResponse和CBCharacteristicWriteWithoutResponse的選擇.
在往藍(lán)牙的某個(gè)特征值寫入數(shù)據(jù)時(shí)用到
- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;
從字面意思上解析:
CBCharacteristicWriteWithResponse: 特征值寫入數(shù)據(jù)會(huì)有相應(yīng).
CBCharacteristicWriteWithoutResponse: 特征值寫入數(shù)據(jù)不會(huì)有響應(yīng).
其實(shí)這個(gè)選擇是有特征值權(quán)限所決定的:
typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) {
CBCharacteristicPropertyBroadcast = 0x01,
CBCharacteristicPropertyRead = 0x02,
CBCharacteristicPropertyWriteWithoutResponse = 0x04,
CBCharacteristicPropertyWrite = 0x08,
CBCharacteristicPropertyNotify = 0x10,
CBCharacteristicPropertyIndicate = 0x20,
CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40,
CBCharacteristicPropertyExtendedProperties = 0x80,
CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0) = 0x100,
CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0) = 0x200
};
特征值權(quán)限可以是多個(gè)結(jié)合,如讀寫共存.

但是我在實(shí)際開發(fā)過程中使用CBCharacteristicWriteWithResponse或CBCharacteristicWriteWithoutResponse對(duì)流程沒有一點(diǎn)差異,后來知道,
在往藍(lán)牙設(shè)備中寫入數(shù)據(jù)時(shí),
2.這是一個(gè)比較討厭還未知道真正原因的問題,當(dāng)時(shí)在做藍(lán)牙功能開發(fā)的時(shí)候,藍(lán)牙模塊代碼封裝中,CBCentralManager對(duì)象是維持一個(gè),還是每一次用到藍(lán)牙功能都去初始化一下,在系統(tǒng)控制臺(tái)中看到,每一次生成一個(gè)CBCentralManager對(duì)象,都會(huì)輸出以下日志:
Jan 23 21:36:36 hende-iPhone blueTest(CoreBluetooth)[4533] <Error>: API MISUSE: <private> has no restore identifier but the delegate implements the centralManager:willRestoreState: method. Restoring will not be supported
Jan 23 21:36:36 hende-iPhone BTServer[61] <Notice>: Received XPC message "CBMsgIdCheckIn" from session ""
Jan 23 21:36:36 hende-iPhone BTServer[61] <Notice>: Received XPC check-in from session "com.wesk.blueTest-central-4533-0"
Jan 23 21:36:36 hende-iPhone BTServer[61] <Notice>: Sending 'session attached' event for session "com.wesk.blueTest-central-4533-0"
Jan 23 21:36:36 hende-iPhone BTServer[61] <Notice>: Registering central session "com.wesk.blueTest-central-4533-0" with backgrounding: on, persistence: off
可以看到一個(gè)CBCentralManager對(duì)象,對(duì)手機(jī)而言,就是一個(gè)session會(huì)話,如果每一次使用完之后即時(shí)釋放該對(duì)象,應(yīng)該不會(huì)有什么問題,而且CBCentralManager對(duì)象每一次初始化的話,手機(jī)系統(tǒng)會(huì)對(duì)藍(lán)牙權(quán)限沒有打開的用戶彈出提示,這樣做設(shè)計(jì)上更人性化.
開發(fā)測(cè)試上線,公司測(cè)試通過,沒有出現(xiàn)任何問題,然后上線,一個(gè)月后iOS手機(jī)用戶基數(shù)達(dá)到2000+了,有一個(gè)iPhoneX iOS11.2的用戶反饋說app藍(lán)牙用不了,打開手機(jī)點(diǎn)擊使用藍(lán)牙功能,提示藍(lán)牙權(quán)限未打開,通過網(wǎng)上所說的各種藍(lán)牙解決方案,多次開關(guān)藍(lán)牙按鈕,手機(jī)飛行模式,手機(jī)重啟... 還是提示權(quán)限未打開!當(dāng)問題到這兒了,我心中是一萬個(gè)不相信是代碼層面問題,因?yàn)?000多的用戶就這么一個(gè)出問題.
隨著用戶的增長(zhǎng),有一個(gè)iPhone7反饋藍(lán)牙也不能正常,使用提示打開藍(lán)牙權(quán)限,問題就變得嚴(yán)重起來了,需要徹底解決這個(gè)問題.
為了繼續(xù)追蹤這個(gè)問題,,特地加了土豪用戶為好友,讓其幫忙測(cè)試找問題,經(jīng)過來回幾輪驗(yàn)證,該手機(jī)拿到系統(tǒng)藍(lán)牙權(quán)限的回調(diào)都是CBManagerStatePoweredOff,期間為了保證環(huán)境干凈,專門寫了一個(gè)獲取藍(lán)牙權(quán)限的demo,一打開App就獲取藍(lán)牙權(quán)限,狀態(tài)實(shí)時(shí)提示,讓他裝起來測(cè)試,權(quán)限獲取居然是正常的,這不是說明我的代碼有問題!!! 心中一萬個(gè)急啊,不該啊,藍(lán)牙代碼使用都一樣,各種比較,從工程配置到代碼細(xì)節(jié),最后得出一個(gè)可能的結(jié)論“CBCentralManager對(duì)象在一個(gè)app中不能多次生成,僅保持一個(gè)CBCentralManager對(duì)象”. 花了一些時(shí)間,將CBCentralManager對(duì)象單例化,重新打包給iPhoneX用戶使用,終于正常了!
但是其原因到現(xiàn)在還是不能非常很好的理解,因?yàn)槌霈F(xiàn)這個(gè)問題的手機(jī)實(shí)在太少,且都是iOS11以上版本,就那么兩只,當(dāng)時(shí)3200+的iPhone手機(jī)用戶,出現(xiàn)該問題的手機(jī)低于千分之一,讓我不得不懷疑是手機(jī)硬件藍(lán)牙問題不兼容.蘋果也沒有指出CBCentralManager對(duì)象不可以同時(shí)存在多個(gè).
上面第一個(gè)問題是開發(fā)過程中遇到過的坑,第二個(gè)是一個(gè)比較嚴(yán)重的問題,應(yīng)該可以歸結(jié)于代碼使用姿勢(shì)了(雖然不知道原因,但為了避免出現(xiàn)手機(jī)藍(lán)牙權(quán)限獲取不正確的場(chǎng)景,CBCentralManager對(duì)象還是使用單例模式吧).
PS:iOS11藍(lán)牙開關(guān)分未設(shè)置頁和控制中心的,設(shè)置頁是總開關(guān)(系統(tǒng)級(jí)使用),控制中心是給各個(gè)APP使用的, 控制中心的開關(guān)有時(shí)候會(huì)顯示異常,顯示打開,但權(quán)限其實(shí)是關(guān)閉的.如下圖

查找解決期間在官方bug反饋上也看到了該問題:https://forums.developer.apple.com/thread/92997.