iOS 藍(lán)牙4.0開發(fā)踩坑總結(jié)

藍(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.CBCharacteristicWriteWithResponseCBCharacteristicWriteWithoutResponse的選擇.
在往藍(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é)合,如讀寫共存.


藍(lán)牙特征值.jpg

但是我在實(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)閉的.如下圖

iOS11展示藍(lán)牙異常.gif

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • iOS開發(fā)藍(lán)牙4.0初識(shí)轉(zhuǎn)載 2015-09-20 15:26:44標(biāo)簽:ios開發(fā)藍(lán)牙ios開發(fā)藍(lán)牙4.0ios...
    Jany_4a9a閱讀 3,026評(píng)論 0 3
  • 什么是藍(lán)牙? 隨著藍(lán)牙低功耗技術(shù)BLE(Bluetooth Low Energy)的發(fā)展,藍(lán)牙技術(shù)正在一步步成熟,...
    一字碼閱讀 1,773評(píng)論 0 11
  • 安靜的夜,同樣的失眠,一個(gè)人開著車漫無目的的走著,下一站是哪?會(huì)遇到誰?誰知道呢?突然想起《從你的全世界路過》里面...
    black_swan閱讀 722評(píng)論 1 1
  • 有國(guó)人人說:我砸爛了我的蘋果手機(jī),改用國(guó)產(chǎn)手機(jī),看看我是多么愛國(guó)!;有國(guó)人說,我砸爛了別人的進(jìn)口汽車,還打傷了...
    碧海飛鴻2016閱讀 356評(píng)論 0 2

友情鏈接更多精彩內(nèi)容