繁体   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