简体   繁体   English

iOS - BLE在背景上扫描随机冻结

[英]iOS - BLE Scanning on background freezes randomly

UPDATE 14/08 - 3 - Found the real solution: 更新14/08 - 3 - 找到真正的解决方案:

You can check out the solution in the answers below ! 您可以在下面的答案中查看解决方案!

UPDATE 16/06 - 2 - May be the solution : 更新16/06 - 2 - 可能是解决方案:

As Sandy Chapman suggested in comments in his answer, i'm able now to retrieve my peripheral at the start of a scan by using this method : 正如桑迪查普曼在回答中的评论中所说,我现在能够在扫描开始时使用这种方法检索我的外围设备:

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

I'm actually trying to make it work by getting my peripheral back at the start of a scan and launching a connection (even if it's not in range) when i need it. 我实际上是想通过在扫描开始时让我的外围设备恢复并在需要时启动连接(即使它不在范围内)来使其工作。 iOS will keep it alive until it finds the device i'm looking for. iOS将保持活着,直到找到我正在寻找的设备。

Please also note that there might be a bug in iOS 8.x that keep an app with debug build from scanning (disappearing callbacks as i get) from time to time if there is another app in background with release build that is using Bluetooth. 另请注意,iOS 8.x中可能存在一个错误,如果在后台有另一个使用蓝牙的发布版本的应用程序,则会不时通过扫描(我得到的消失回调)保留一个带有调试版本的应用程序。

UPDATE 16/06 : 更新16/06:

So i checked with retrievePeripheralsWithServices if any device was connected while i'm starting the scan. 所以我检查了retrievePeripheralsWithServices,如果在我开始扫描时连接了任何设备。 When i get the bug, i launch the app and the first thing i do in 当我得到错误时,我启动应用程序和我做的第一件事

- (void) applicationDidBecomeActive:(UIApplication *)application

is to check the size of the returned array. 是检查返回数组的大小。 It's always 0, each time i get the bug. 它总是0,每次我得到错误。 The bug can happen also if my device hasn't made any connection earlier in the current run. 如果我的设备在当前运行中之前没有建立任何连接,也会发生错误。 I'm also able to see my device advertising and trigger a command with a second device while i got the bug with another device. 当我收到另一台设备的错误时,我也能看到我的设备广告并用第二台设备触发命令。

UPDATE 10/06 : 更新10/06:

  • I left my app running the entire night to check if there wasn't any memory leak or massive resource usage, here are my result after ~12-14 hours of running in the background. 我让我的应用程序运行整晚以检查是否没有任何内存泄漏或大量资源使用,这是我在后台运行~12-14小时后的结果。 Memory/CPU usage are exactly the same as they were when i left. 内存/ CPU使用情况与我离开时完全相同。 It leads me to think my app hasn't any leak that could lead iOS to close it to get back memory/CPU usage. 这让我觉得我的应用程序没有任何泄漏可能导致iOS关闭它以恢复内存/ CPU使用率。

资源使用分析

UPDATE 08/06 : 更新08/06:

  • Note that it's not an advertising issue since our BLE device is consistently powered, and we used the strongest BLE electronic card we could found. 请注意,这不是一个广告问题,因为我们的BLE设备始终处于供电状态,我们使用了我们能找到的最强的BLE电子卡。
  • It's also not an issue with iOS detection timing in background. 在后台iOS检测时间也不是问题。 I waited for a very long time(20~30min) to be sure that wasn't this issue. 我等了很长时间(20~30分钟)才确定不是这个问题。

ORIGINAL QUESTION 原始问题

I'm currently working on an app that handle communication with a BLE device. 我目前正在开发一个处理与BLE设备通信的应用程序。 One of my constraint is that i must connect to this device only when i have to send a command or read data. 我的一个限制是,只有在必须发送命令或读取数据时才必须连接到此设备。 I must disconnect as soon as possible when it's done, to allow other potential users to do the same. 我必须在完成后尽快断开连接,以允许其他潜在用户也这样做。

One of the features of the app is the following : 该应用程序的一个功能如下:

  • Users can enable an automatic command while the app is in the background. 用户可以在应用程序处于后台时启用自动命令。 This automatic command triggers if the device hasn't been detected within 10 minutes. 如果在10分钟内未检测到设备,则会触发此自动命令。
  • My app scans until it finds my BLE device. 我的应用会扫描,直到找到我的BLE设备。
  • In order to keep it awake when i need it, i'm restarting the scan each time, because of the CBCentralManagerScanOptionAllowDuplicatesKey option ignorance. 为了在需要时保持清醒,我每次都重新开始扫描,因为CBCentralManagerScanOptionAllowDuplicatesKey选项无知。
  • When it's detected, i'm checking if the last detection was more than 10 minutes ago. 当它被检测到时,我正在检查最后一次检测是否超过10分钟。 If that's the case, i connect to the device and then write into the characteristic corresponding to the service i need. 如果是这种情况,我连接到设备,然后写入对应于我需要的服务的特性。

The goal is to trigger this device when user come in range. 目标是在用户进入范围时触发此设备。 It may happen a few minutes after being out of range such as a few hours, it depends on my users habits. 它可能会在超出范围几分钟后发生几个小时,这取决于我的用户习惯。

Everything is working fine this way, but sometimes (it seems like happening at random times), the scan sort of "freezes". 一切都以这种方式运作良好,但有时(似乎在随机时间发生),扫描类型“冻结”。 My process is done well, but after a couple of time, i see my app scanning, but my didDiscoverPeripheral: call back is never called, even if my testing device is right in front of my BLE device. 我的过程做得很好,但经过几次,我看到我的应用程序扫描,但我的didDiscoverPeripheral:回调从未被调用,即使我的测试设备正好在我的BLE设备前面。 Sometimes it may take a while to detect it, but here, nothing happens after a couple of minutes. 有时它可能需要一段时间来检测它,但在这里,几分钟后没有任何反应。

I was thinking that iOS may have killed my app to claim back memory, but when i turn off and on Bluetooth, centralManagerDidUpdateState: is called the right way. 我以为iOS可能已经杀死我的应用程序以声明内存,但当我关闭和蓝牙时, centralManagerDidUpdateState:被称为正确的方式。 If my app where killed, it shouldn't be the case right ? 如果我的应用程序被杀,那不应该是这样吗? If i open my app, the scan is restarted and it's coming back to life. 如果我打开我的应用程序,扫描将重新启动,它将恢复生机。 I also checked that iOS doesn't shutdown my app after 180 seconds of activity, but that's not the case because it's working well after this amount of time. 我还检查了iOS在180秒的活动后没有关闭我的应用程序,但事实并非如此,因为它在这段时间后运行良好。

I've set up my .plist to have the right settings ( bluetooth-central in UIBackgroundModes ). 我已经设置了我的.plist以获得正确的设置( UIBackgroundModes中的 bluetooth-central )。 My class managing all BLE processing is stored in my AppDelegate as a singleton accessible through all of my app. 管理所有BLE处理的我的班级作为单身人士存储在我的AppDelegate中 ,可通过我的所有应用程序访问。 I've also tested to switch where i'm creating this object. 我也测试过切换我正在创建这个对象的地方。 Currently i'm creating it in the application:didFinishLaunchingWithOptions: method. 目前我正在应用程序中创建它:didFinishLaunchingWithOptions:方法。 I tried to put it in my AppDelegate init: but the scans fails every time while i'm in background if i do so. 我试着将它放在我的AppDelegate init中:但是如果我这样做的话,每次我在后台时扫描都会失败。

I don't know which part of my code i could show you to help you better understand my process. 我不知道我的代码的哪一部分可以告诉你,以帮助你更好地理解我的过程。 Here is some samples that might help. 以下是一些可能有用的示例。 Please note that " AT_appDelegate " is a maccro in order to access my AppDelegate . 请注意,“ 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];
}
  }

In your sample code, it doesn't look like you're calling either of these methods on your CBCentralManager: 在您的示例代码中,它看起来不像您在CBCentralManager上调用这些方法中的任何一个:

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

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

It's possible that you're in a state where you're waiting on a didDiscoverPeripheral that will never come because the system is already connected. 您可能正处于等待由于系统已连接而永远不会出现的didDiscoverPeripheral的状态。 After instantiating your CBCentralManager, first check to see if your peripheral is already connected by calling one of these methods. 在实例化CBCentralManager之后,首先通过调用其中一种方法检查外围设备是否已连接。

The start-up flow for my central manager which uses state restoration is as follows: 我的中央管理员使用状态恢复的启动流程如下:

  1. Check for available peripherals from the restoration state (not needed in your case). 检查恢复状态下的可用外围设备(在您的情况下不需要)。
  2. Check for peripherals discovered from a previous scan (I keep these in an array of available peripherals). 检查从先前扫描中发现的外围设备(我将这些外设保存在可用外设阵列中)。
  3. Check for connected peripherals (using methods mentioned above). 检查连接的外围设备(使用上述方法)。
  4. Begin scanning if none of the above returned a peripheral. 如果以上都没有返回外设,则开始扫描。

One additional thing tip that you may have already discovered: iOS will cache the characteristics and UUIDs advertised by a peripheral. 您可能已经发现了一个额外的提示:iOS将缓存外围设备公布的特征和UUID。 If these change, the only way to clear the cache is to toggle bluetooth off and on in the iOS system settings. 如果这些更改,清除缓存的唯一方法是在iOS系统设置中关闭和打开蓝牙。

After many many tries, i may finally have found the real solution. 经过多次尝试,我终于找到了真正的解决方案。

FYI, i had many talks with an engineer at Apple (via Technical Support). 仅供参考,我与Apple的一位工程师(通过技术支持)进行了多次会谈。 He led me in two ways : 他以两种方式领导我:

  • Checking that Core Bluetooth preservation / restoration state are correctly implemented. 检查核心蓝牙保留/恢复状态是否正确实现。 You will be able to found a few threads about it on stack overflow like this thread or this one . 你可以在这个线程或者这个 线程的堆栈溢出上找到一些关于它的线程 You will also found apple documentation useful here : Core Bluetooth Background Processing . 您还可以在此处找到有用的Apple文档: 核心蓝牙后台处理
  • Checking connections parameters implemented in my Bluetooth device, such as : Connection Interval , Minimum and Maximum connection interval , Slave latency and Connection timeout . 检查我的蓝牙设备中实现的连接参数,例如: 连接间隔最小和最大连接间隔从设备延迟连接超时 There is much information in Apple Bluetooth Design Guidelines Apple蓝牙设计指南中有很多信息

The real solution may take part in these elements. 真正的解决方案可以参与这些要素。 I added and corrected things while i was searching to clean this bug. 我在搜索清理这个bug时添加并更正了一些内容。 But the thing that made it work (i tested it for around 4-5h and it didn't froze at all) was about dispatch_queue . 但是让它发挥作用的东西(我测试了大约4-5小时并且它根本没有冻结)是关于dispatch_queue

What i did before : 我之前做过的事:

// 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];
});

What i'm doing now : 我现在在做什么:

// 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];
});

Please note that i added this line to my DeviceManager.h (my main Ble class) : 请注意我将此行添加到我的DeviceManager.h(我的主要Ble类):

@property (atomic, strong) dispatch_queue_t bluetoothQueue;

As you can see it was a little messed up :) 你可以看到它有点混乱:)

So now i'm able to scan as long as needed. 所以现在我可以根据需要进行扫描。 Thanks for your help ! 谢谢你的帮助 ! I hope it might help someone one day. 我希望有一天它可能对某人有所帮助。

It could be related to the Bluetooth device you are trying to pair with, some of them have a low advertising rate, this is usually done to save battery-life. 它可能与您尝试配对的蓝牙设备有关,其中一些具有较低的广告费率,这通常是为了节省电池寿命。 The first thing I'd check is how often the BLE device is configured to advertise. 我要检查的第一件事是BLE设备配置广告的频率。

This may even be related to the iOS device not scanning as frequently as other devices, as covered in this post . 这甚至可能与iOS设备不像其他设备那样频繁扫描有关,如本文所述

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM