簡體   English   中英

iOS - BLE在背景上掃描隨機凍結

[英]iOS - BLE Scanning on background freezes randomly

更新14/08 - 3 - 找到真正的解決方案:

您可以在下面的答案中查看解決方案!

更新16/06 - 2 - 可能是解決方案:

正如桑迪查普曼在回答中的評論中所說,我現在能夠在掃描開始時使用這種方法檢索我的外圍設備:

- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers

我實際上是想通過在掃描開始時讓我的外圍設備恢復並在需要時啟動連接(即使它不在范圍內)來使其工作。 iOS將保持活着,直到找到我正在尋找的設備。

另請注意,iOS 8.x中可能存在一個錯誤,如果在后台有另一個使用藍牙的發布版本的應用程序,則會不時通過掃描(我得到的消失回調)保留一個帶有調試版本的應用程序。

更新16/06:

所以我檢查了retrievePeripheralsWithServices,如果在我開始掃描時連接了任何設備。 當我得到錯誤時,我啟動應用程序和我做的第一件事

- (void) applicationDidBecomeActive:(UIApplication *)application

是檢查返回數組的大小。 它總是0,每次我得到錯誤。 如果我的設備在當前運行中之前沒有建立任何連接,也會發生錯誤。 當我收到另一台設備的錯誤時,我也能看到我的設備廣告並用第二台設備觸發命令。

更新10/06:

  • 我讓我的應用程序運行整晚以檢查是否沒有任何內存泄漏或大量資源使用,這是我在后台運行~12-14小時后的結果。 內存/ CPU使用情況與我離開時完全相同。 這讓我覺得我的應用程序沒有任何泄漏可能導致iOS關閉它以恢復內存/ CPU使用率。

資源使用分析

更新08/06:

  • 請注意,這不是一個廣告問題,因為我們的BLE設備始終處於供電狀態,我們使用了我們能找到的最強的BLE電子卡。
  • 在后台iOS檢測時間也不是問題。 我等了很長時間(20~30分鍾)才確定不是這個問題。

原始問題

我目前正在開發一個處理與BLE設備通信的應用程序。 我的一個限制是,只有在必須發送命令或讀取數據時才必須連接到此設備。 我必須在完成后盡快斷開連接,以允許其他潛在用戶也這樣做。

該應用程序的一個功能如下:

  • 用戶可以在應用程序處於后台時啟用自動命令。 如果在10分鍾內未檢測到設備,則會觸發此自動命令。
  • 我的應用會掃描,直到找到我的BLE設備。
  • 為了在需要時保持清醒,我每次都重新開始掃描,因為CBCentralManagerScanOptionAllowDuplicatesKey選項無知。
  • 當它被檢測到時,我正在檢查最后一次檢測是否超過10分鍾。 如果是這種情況,我連接到設備,然后寫入對應於我需要的服務的特性。

目標是在用戶進入范圍時觸發此設備。 它可能會在超出范圍幾分鍾后發生幾個小時,這取決於我的用戶習慣。

一切都以這種方式運作良好,但有時(似乎在隨機時間發生),掃描類型“凍結”。 我的過程做得很好,但經過幾次,我看到我的應用程序掃描,但我的didDiscoverPeripheral:回調從未被調用,即使我的測試設備正好在我的BLE設備前面。 有時它可能需要一段時間來檢測它,但在這里,幾分鍾后沒有任何反應。

我以為iOS可能已經殺死我的應用程序以聲明內存,但當我關閉和藍牙時, centralManagerDidUpdateState:被稱為正確的方式。 如果我的應用程序被殺,那不應該是這樣嗎? 如果我打開我的應用程序,掃描將重新啟動,它將恢復生機。 我還檢查了iOS在180秒的活動后沒有關閉我的應用程序,但事實並非如此,因為它在這段時間后運行良好。

我已經設置了我的.plist以獲得正確的設置( UIBackgroundModes中的 bluetooth-central )。 管理所有BLE處理的我的班級作為單身人士存儲在我的AppDelegate中 ,可通過我的所有應用程序訪問。 我也測試過切換我正在創建這個對象的地方。 目前我正在應用程序中創建它:didFinishLaunchingWithOptions:方法。 我試着將它放在我的AppDelegate init中:但是如果我這樣做的話,每次我在后台時掃描都會失敗。

我不知道我的代碼的哪一部分可以告訴你,以幫助你更好地理解我的過程。 以下是一些可能有用的示例。 請注意,“ AT_appDelegate ”是一個maccro,以便訪問我的AppDelegate

// Init of my DeviceManager class that handles all BLE processing
- (id) init {
   self = [super init];

   // Flags creation
   self.autoConnectTriggered = NO;
   self.isDeviceReady = NO;
   self.connectionUncomplete = NO;
   self.currentCommand = NONE;
   self.currentCommand_index = 0;

   self.signalOkDetectionCount = 0; // Helps to find out if device is at a good range or too far
   self.connectionFailedCount = 0;  // Helps in a "try again" process if a command fails

   self.main_uuid = [CBUUID UUIDWithString:MAINSERVICE_UUID];
   self.peripheralsRetainer = [[NSMutableArray alloc] init];
   self.lastDeviceDetection = nil;

   // Ble items creation
   dispatch_queue_t queue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
   self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue];

[self startScanning];
return self;
}

   // The way i start the scan
- (void) startScanning {

   if (!self.isScanning && self.centralManager.state == CBCentralManagerStatePoweredOn) {

    CLS_LOG(@"### Start scanning ###");
    self.isScanning = YES;

    NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:!self.isBackground] forKey:CBCentralManagerScanOptionAllowDuplicatesKey];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    [self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];

    });
  }
  }

  // The way i stop and restart the scan after i've found our device. Contains    some of foreground (UI update) process that you can ignore
  - (void) stopScanningAndRestart: (BOOL) restart {

  CLS_LOG(@"### Scanning terminated ###");
  if (self.isScanning) {

    self.isScanning = NO;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    [self.centralManager stopScan];
  });

  // Avoid clearing the connection when waiting for notification (remote + learning)

     if (!self.isWaitingNotifiy && !self.isSynchronizing && self.currentCommand == NONE ) {
        // If no device found during scan, update view

        if (self.deviceToReach == nil && !self.isBackground) {

            // Check if any connected devices last
            if (![self isDeviceStillConnected]) {
               CLS_LOG(@"--- Device unreachable for view ---");

            } else {

                self.isDeviceInRange = YES;
                self.deviceToReach = AT_appDelegate.user.device.blePeripheral;
            }

            [self.delegate performSelectorOnMainThread:@selector(updateView) withObject:nil waitUntilDone:YES];       

        }

        // Reset var
        self.deviceToReach = nil;
        self.isDeviceInRange = NO;
        self.signalOkDetectionCount = 0;

        // Check if autotrigger needs to be done again - If time interval is higher enough,
        // reset autoConnectTriggered to NO. If user has been away for <AUTOTRIGGER_INTERVAL>
        // from the device, it will trigger again next time it will be detected.

        if ([[NSDate date] timeIntervalSinceReferenceDate] - [self.lastDeviceDetection timeIntervalSinceReferenceDate] > AUTOTRIGGER_INTERVAL) {

            CLS_LOG(@"### Auto trigger is enabled ###");
            self.autoConnectTriggered = NO;
        }
    }
}


   if (restart) {
    [self startScanning];
   }
  }

  // Here is my detection process, the flag "isInBackground" is set up each    time the app goes background
  - (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {

CLS_LOG(@"### : %@ -- %@", peripheral.name, RSSI);
BOOL deviceAlreadyShown = [AT_appDelegate isDeviceAvailable];

// If current device has no UUID set, check if peripheral is the right one
// with its name, containing his serial number (macaddress) returned by
// the server on remote adding

NSString *p1 = [[[peripheral.name stringByReplacingOccurrencesOfString:@":" withString:@""] stringByReplacingOccurrencesOfString:@"Extel " withString:@""] uppercaseString];

NSString *p2 = [AT_appDelegate.user.device.serial uppercaseString];

if ([p1 isEqualToString:p2]) {
    AT_appDelegate.user.device.scanUUID = peripheral.identifier;
}

// Filter peripheral connection with uuid
if ([AT_appDelegate.user.device.scanUUID isEqual:peripheral.identifier]) {
    if (([RSSI intValue] > REQUIRED_SIGNAL_STRENGTH && [RSSI intValue] < 0) || self.isBackground) {
        self.signalOkDetectionCount++;
        self.deviceToReach = peripheral;
        self.isDeviceInRange = (self.signalOkDetectionCount >= REQUIRED_SIGNAL_OK_DETECTIONS);

        [peripheral setDelegate:self];
        // Reset blePeripheral if daughter board has been switched and there were
        // not enough time for the software to notice connection has been lost.
        // If that was the case, the device.blePeripheral has not been reset to nil,
        // and might be different than the new peripheral (from the new daugtherboard)

       if (AT_appDelegate.user.device.blePeripheral != nil) {
            if (![AT_appDelegate.user.device.blePeripheral.name isEqualToString:peripheral.name]) {
                AT_appDelegate.user.device.blePeripheral = nil;
            }
        }

        if (self.lastDeviceDetection == nil ||
            ([[NSDate date] timeIntervalSinceReferenceDate] - [self.lastDeviceDetection timeIntervalSinceReferenceDate] > AUTOTRIGGER_INTERVAL)) {
            self.autoConnectTriggered = NO;
        }

        [peripheral readRSSI];
        AT_appDelegate.user.device.blePeripheral = peripheral;
        self.lastDeviceDetection = [NSDate date];

        if (AT_appDelegate.user.device.autoconnect) {
            if (!self.autoConnectTriggered && !self.autoTriggerConnectionLaunched) {
                CLS_LOG(@"--- Perform trigger ! ---");

                self.autoTriggerConnectionLaunched = YES;
                [self executeCommand:W_TRIGGER onDevice:AT_appDelegate.user.device]; // trigger !
                return;
            }
        }
    }

    if (deviceAlreadyShown) {
        [self.delegate performSelectorOnMainThread:@selector(updateView) withObject:nil waitUntilDone:YES];
    }
}

if (self.isBackground && AT_appDelegate.user.device.autoconnect) {
    CLS_LOG(@"### Relaunch scan ###");
    [self stopScanningAndRestart:YES];
}
  }

在您的示例代碼中,它看起來不像您在CBCentralManager上調用這些方法中的任何一個:

- (NSArray<CBPeripheral *> * nonnull)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> * nonnull)serviceUUIDs

- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers

您可能正處於等待由於系統已連接而永遠不會出現的didDiscoverPeripheral的狀態。 在實例化CBCentralManager之后,首先通過調用其中一種方法檢查外圍設備是否已連接。

我的中央管理員使用狀態恢復的啟動流程如下:

  1. 檢查恢復狀態下的可用外圍設備(在您的情況下不需要)。
  2. 檢查從先前掃描中發現的外圍設備(我將這些外設保存在可用外設陣列中)。
  3. 檢查連接的外圍設備(使用上述方法)。
  4. 如果以上都沒有返回外設,則開始掃描。

您可能已經發現了一個額外的提示:iOS將緩存外圍設備公布的特征和UUID。 如果這些更改,清除緩存的唯一方法是在iOS系統設置中關閉和打開藍牙。

經過多次嘗試,我終於找到了真正的解決方案。

僅供參考,我與Apple的一位工程師(通過技術支持)進行了多次會談。 他以兩種方式領導我:

  • 檢查核心藍牙保留/恢復狀態是否正確實現。 你可以在這個線程或者這個 線程的堆棧溢出上找到一些關於它的線程 您還可以在此處找到有用的Apple文檔: 核心藍牙后台處理
  • 檢查我的藍牙設備中實現的連接參數,例如: 連接間隔最小和最大連接間隔從設備延遲連接超時 Apple藍牙設計指南中有很多信息

真正的解決方案可以參與這些要素。 我在搜索清理這個bug時添加並更正了一些內容。 但是讓它發揮作用的東西(我測試了大約4-5小時並且它根本沒有凍結)是關於dispatch_queue

我之前做過的事:

// Initialisation
dispatch_queue_t queue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue options:@{CBCentralManagerOptionRestoreIdentifierKey:RESTORE_KEY}];

// Start scanning
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   [self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});

// Stop scanning
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   [self.centralManager stopScan];
});

我現在在做什么:

// Initialisation
self.bluetoothQueue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
    self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:self.bluetoothQueue options:@{CBCentralManagerOptionRestoreIdentifierKey:RESTORE_KEY}];

// Start scanning
dispatch_async(self.bluetoothQueue, ^{
   [self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});

// Stop scanning
dispatch_async(self.bluetoothQueue, ^{
   [self.centralManager stopScan];
});

請注意我將此行添加到我的DeviceManager.h(我的主要Ble類):

@property (atomic, strong) dispatch_queue_t bluetoothQueue;

你可以看到它有點混亂:)

所以現在我可以根據需要進行掃描。 謝謝你的幫助 ! 我希望有一天它可能對某人有所幫助。

它可能與您嘗試配對的藍牙設備有關,其中一些具有較低的廣告費率,這通常是為了節省電池壽命。 我要檢查的第一件事是BLE設備配置廣告的頻率。

這甚至可能與iOS設備不像其他設備那樣頻繁掃描有關,如本文所述

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM