CoreBluetooth 藍牙開發(fā)(后臺模式、狀態(tài)保存與恢復)

最近新進一家公司,主要是做物聯(lián)網(wǎng)這一塊的的,項目需要用到藍牙開發(fā),講真的,挑戰(zhàn)還是挺大的,做了差不多四年的iOS開發(fā),從沒有接觸過藍牙開發(fā)這一領(lǐng)域,我是這樣學習的。

從網(wǎng)上找各種博客(國內(nèi)的,國外的),借鑒別人寫過的Demo以及官方文檔,花了整整的一周時間,對iOS的CoreBluetooth這個框架的使用稍微有一些的了解,請聽我一一道來;

iOS 藍牙

簡稱:BLE(buletouch low energy),藍牙 4.0 設(shè)備因為低耗電,所以也叫做 BLE,CoreBluetooth框架就是蘋果公司為我們提供的一個庫,我們可以使用這個庫和其他支持藍牙4.0的設(shè)備進行數(shù)據(jù)交互。值得注意的是在IOS10之后的APP中,我們需要在 info.plist文件中添加NSBluetoothPeripheralUsageDescription字段否則APP會崩潰

工作模式:藍牙通信中,首先需要提到的就是 central 和 peripheral 兩個概念。這是設(shè)備在通信過程中扮演的兩種角色。直譯過來就是 [中心] 和 [周邊(可以理解為外設(shè))]。iOS 設(shè)備既可以作為 central,也可以作為 peripheral,這主要取決于通信需求。

自己嘗試的寫了個Demo,實現(xiàn)的功能有:

1、通過已知外圍設(shè)備的服務(wù)UUID搜索(這個UUID是指被廣播出來的服務(wù)UUID);
2、連接指定的外圍設(shè)備;
3、獲取指定的服務(wù),發(fā)現(xiàn)需要訂閱的特征;
4、接收外圍設(shè)備發(fā)送的數(shù)據(jù);
5、向外圍設(shè)備寫數(shù)據(jù);
6、實現(xiàn)藍牙服務(wù)的后臺模式;
7、實現(xiàn)藍牙服務(wù)的狀態(tài)保存與恢復(應(yīng)用被系統(tǒng)殺死的時候,系統(tǒng)會自動保存 central manager 的狀態(tài));

中心角色的實現(xiàn):(central)

(1)、初始化中央管理器對象

/**
第一個參數(shù):代理
第二個參數(shù):隊列(nil為不指定隊列,默認為主隊列)
第三個參數(shù):實現(xiàn)狀態(tài)保存的時候需要用到 eg:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"} 
*/  
centerManager = [[CBCentralManager alloc]initWithDelegate:self queue:queue options:options];

中央管理器會調(diào)用 centralManagerDidUpdateState:通知藍牙的狀態(tài)

(2)、發(fā)現(xiàn)外圍設(shè)備

[centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:SERVICE_UUID]] options:nil];

每次中央管理器發(fā)現(xiàn)外圍設(shè)備時,它都會調(diào)用centralManager:didDiscoverPeripheral:advertisementData:RSSI:其委托對象的方法。

(3)、發(fā)現(xiàn)想要的外圍設(shè)備進行連接

#pragma mark -- 掃描發(fā)現(xiàn)到任何一臺設(shè)備都會通過這個代理方法回調(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(@"藍牙名字:%@  信號強弱:%@",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 -- 連接成功、獲取當前設(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è)備(CBPeripheral你連接的對象)會調(diào)用peripheral:didDiscoverServices:其委托對象的方法。

(5)、發(fā)現(xiàn)服務(wù)的特征

#pragma mark -- 獲取當前設(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:當發(fā)現(xiàn)指定服務(wù)的特征時,外圍設(shè)備調(diào)用其委托對象的方法。

(6)、檢索特征價值

閱讀特征的值 ()

 [peripheral readValueForCharacteristic:interestingCharacteristic];

注意: 并非所有特征都是可讀的。你可以通過檢查其properties屬性是否包含CBCharacteristicPropertyRead常量來確定特征是否可讀。如果嘗試讀取不可讀的特征值,則peripheral:didUpdateValueForCharacteristic:error:委托方法將返回合適的錯誤。

訂閱特征的值()

雖然使用該readValueForCharacteristic:方法讀取特征值對靜態(tài)值有效,但它不是檢索動態(tài)值的最有效方法。檢索隨時間變化的特征值 - 例如,你的心率 - 通過訂閱它們。訂閱特征值時,您會在值更改時收到外圍設(shè)備的通知。

[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];

注意: 并非所有特征都提供訂閱。你可以通過檢查特性是否properties包含其中一個CBCharacteristicPropertyNotify或多個CBCharacteristicPropertyIndicate常量來確定特征是否提供訂閱。
當你訂閱(或取消訂閱)特征的值時,外圍設(shè)備會調(diào)用peripheral:didUpdateNotificationStateForCharacteristic:error:其委托對象的方法。

寫一個特征的值 ()

有時寫一個特征的值是有意義的。例如,如果你的應(yīng)用程序與藍牙低功耗數(shù)字恒溫器交互,你可能需要為恒溫器提供設(shè)置房間溫度的值。如果特征值是可寫的,則可以NSData通過調(diào)用外設(shè)writeValue:forCharacteristic:type:方法將數(shù)據(jù)值;

[self.discoveredPeripheral writeValue:data forCharacteristic:self.characteristic1 type:CBCharacteristicWriteWithResponse];

寫入特征的值時,指定要執(zhí)行的寫入類型。在上面的示例中,寫入類型CBCharacteristicWriteWithResponse指示外圍設(shè)備通過調(diào)用peripheral:didWriteValueForCharacteristic:error:其委托對象的方法讓您的應(yīng)用程序知道寫入是否成功。

外圍角色的實現(xiàn)

(1)、初始化外圍設(shè)備管理器

peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];

創(chuàng)建外圍設(shè)備管理器時,外圍設(shè)備管理器會調(diào)用peripheralManagerDidUpdateState:其委托對象的方法。您必須實現(xià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];

當調(diào)用此方法發(fā)布服務(wù)時,外圍管理器將調(diào)用peripheralManager:didAddService:error:其委托對象的方法。通過error可以知道是否發(fā)布成功;
將服務(wù)及其任何關(guān)聯(lián)特性發(fā)布到外圍設(shè)備的數(shù)據(jù)庫后,該服務(wù)將被緩存,將無法再對其進行更改。

(4)、廣播服務(wù)

  [peripheralManager startAdvertising:@ {CBAdvertisementDataServiceUUIDsKey:@[myFirstService.UUID,mySecondService.UUID]}];

當開始在本地外圍設(shè)備上公布某些數(shù)據(jù)時,外圍設(shè)備管理器會調(diào)用peripheralManagerDidStartAdvertising:error:其委托對象的方法。

(5)、響應(yīng)來自中央的讀取和寫入請求

當連接的中央請求讀取某個特征的值時,外圍管理器會調(diào)用peripheralManager:didReceiveReadRequest:其委托對象的方法。

 [peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset]; 

設(shè)置讀取請求不要求從超出特征值的邊界的索引位置讀取

  request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset,myCharacteristic.value.length  -  request.offset)];  

將請求的特性屬性(默認值為nil)的值設(shè)置為您在本地外圍設(shè)備上創(chuàng)建的特征值,同時考慮讀取請求的偏移量

設(shè)置值后,響應(yīng)遠程中央以指示請求已成功完成。通過調(diào)用類的respondToRequest:withResult:方法CBPeripheralManager,傳回請求(其更新的值)和請求的結(jié)果

當連接的中心發(fā)送寫入一個或多個特征值的請求時,外圍管理器會調(diào)用peripheralManager:didReceiveWriteRequests:其委托對象的方法

(6)、將更新的特征值發(fā)送到訂閱的中心

當連接的中心訂閱某個特征的值時,外圍管理器會調(diào)用peripheralManager:central:didSubscribeToCharacteristic:其委托對象的方法
獲取特征的更新值,并通過調(diào)用類的updateValue:forCharacteristic:onSubscribedCentrals:方法將其發(fā)送到中心CBPeripheralManager。

處理常駐后臺任務(wù)

首先需要在Capabilities-->Background Modes申請中心角色的后臺模式說明

如圖:


中心角色后臺模式.jpg

(1)、狀態(tài)保存與恢復

因為狀態(tài)的保存和恢復 Core Bluetooth 都為我們封裝好了,所以我們只需要選擇是否需要這個特性即可。系統(tǒng)會保存當前 central manager 或 peripheral manager,并且繼續(xù)執(zhí)行藍牙相關(guān)事件(即使程序已經(jīng)不再運行)。一旦事件執(zhí)行完畢,系統(tǒng)會在后臺重啟 app,這時你有機會去存儲當前狀態(tài),并且處理一些事物。在之前提到的 “門鎖” 的例子中,系統(tǒng)會監(jiān)視連接請求,并在 centralManager:didConnectPeripheral: 回調(diào)時,重啟 app,在用戶回家后,連接操作結(jié)束。

Core Bluetooth 的狀態(tài)保存與恢復在設(shè)備作為 central、peripheral 或者這兩種角色時,都可用。在設(shè)備作為 central 并添加了狀態(tài)保存與恢復支持后,如果 app 被強行關(guān)閉進程,系統(tǒng)會自動保存 central manager 的狀態(tài)(如果 app 有多個 central manager,你可以選擇哪一個需要系統(tǒng)保存)。

對于 CBCentralManager,系統(tǒng)會保存以下信息:

central 準備連接或已經(jīng)連接的 peripheral
central 需要掃描的 service(包括掃描時,配置的 options)
central 訂閱的 characteristic
對于 peripheral 來說,情況也差不多。系統(tǒng)對 CBPeripheralManager 的處理方式如下:
peripheral 在廣播的數(shù)據(jù)
peripheral 存入的 service 和 characteristic 的樹形結(jié)構(gòu)
已經(jīng)被 central 訂閱了的 characteristic 的值
當系統(tǒng)在后臺重新加載程序后(可能是因為找到了要找的 peripheral),你可以重新實例化 central manager 或 peripheral 并恢復他們的狀態(tài)。

(2)、選擇支持存儲和恢復

如果要支持存儲和恢復,則需要在初始化 manager 的時候給一個 restoration identifier。restoration identifier 是 string 類型,并標識了 app 中的 central manager 或 peripheral manager。這個 string 很重要,它將會告訴 Core Bluetooth 需要存儲狀態(tài),畢竟 Core Bluetooth 恢復有 identifier 的對象。

例如,在 central 端,要想支持該特性,可以在調(diào)用 CBCentralManager 的初始化方法時,配置 CBCentralManagerOptionRestoreIdentifierKey:

centralManager = [[CBCentralManager alloc] initWithDelegate:self 
queue:nil
options:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"}];

雖然以上代碼沒有展示出來,其實在 peripheral manager 中要設(shè)置 identifier 也是這樣的。只是在初始化時,將 key 改成了 CBPeripheralManagerOptionRestoreIdentifierKey。
因為程序可以有多個 CBCentralManager 和 CBPeripheralManager,所以要確保每個 identifier 都是唯一的。

(3)、重新初始化 central manager 和 peripheral manager

當系統(tǒng)重新在后臺加載程序時,首先需要做的即根據(jù)存儲的 identifier,重新初始化 central manager 或 peripheral manager。如果你只有一個 manager,并且 manager 存在于 app 生命周期中,那這個步驟就不需要做什么了。
.
如果 app 中包含多個 manager,或者 manager 不是在整個 app 生命周期中都存在的,那 app 就必須要區(qū)分你要重新初始化哪個 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)啟動項目");
            //在這里創(chuàng)建的藍牙實例一定要被當前類持有,不然出了這個函數(shù)就被銷毀了,藍牙檢測會出現(xiàn)“XPC connection invalid”
            self.bluetooth = [[MSBBlueTooth alloc]initWithQueue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey : identifier}];
            NSLog(@"");
        }
    }

return YES;
}

(4)、實現(xiàn)恢復狀態(tài)的代理方法

在重新初始化 manager 之后,接下來需要同步 Core Bluetooth 存儲的他們的狀態(tài)。要想弄清楚在程序被退出時都在做些什么,就需要正確的實現(xiàn)代理方法。對于 central manager 來說,需要實現(xiàn) centralManager:willRestoreState:;對于 peripheral manager 來說,需要實現(xiàn) peripheralManager:willRestoreState:。
.
注意:如果選擇存儲和恢復狀態(tài),當系統(tǒng)在后臺重新加載程序時,首先調(diào)用的方法是 centralManager:willRestoreState: 或 peripheralManager:willRestoreState:。如果沒有選擇存儲的恢復狀態(tài)(或者喚醒時沒有什么內(nèi)容需要恢復),那么首先調(diào)用的方法是 centralManagerDidUpdateState: 或 peripheralManagerDidUpdateState:。
.
無論是以上哪種代理方法,最后一個參數(shù)都是一個包含程序退出前狀態(tài)的字典。字典中,可用的 key ,

central 端有:
NSString *const CBCentralManagerRestoredStatePeripheralsKey;
NSString *const CBCentralManagerRestoredStateScanServicesKey;
NSString *const CBCentralManagerRestoredStateScanOptionsKey;

peripheral 端有:
NSString *const CBPeripheralManagerRestoredStateServicesKey;
NSString *const CBPeripheralManagerRestoredStateAdvertisementDataKey;

要恢復 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è)備加入列表,在藍牙檢測狀態(tài)的回調(diào)里實現(xiàn)重連
    self.peripherals = [NSMutableArray arrayWithArray:peripherals];

}

具體要對拿到的 peripheral 數(shù)組做什么就要根據(jù)需求來了。如果這是個 central manager 搜索到的 peripheral 數(shù)組,那就可以存儲這個數(shù)組的引用,并且開始建立連接了(注意給這些 peripheral 設(shè)置代理,否則連接后不會走 peripheral 的代理方法)。
.
恢復 peripheral manager 的狀態(tài)和 central manager 的方式類似,就只是把代理方法換成了 peripheralManager:willRestoreState:,并且使用對應(yīng)的 key 即可

寫的不是很好,也算是東拼西湊了,但也是花了時間去整理的,如果看不懂,可以下載我的Demo自己跑一遍;

想要看實現(xiàn)效果,可以下載Demo,看的再多也不如項目跑一遍來的快,療效是不騙人的;

有需要的可以加我微信Jarvis-LLL,一起討論學習

喜歡就點個贊,也可以在下方評論一起討論討論

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內(nèi)容