iOS 11 - CoreBluetooth Broken If Using State Presevation and Peripheral Not Cancelled Properly

When using state preservation in iOS 11, if a peripheral connection is not cancelled properly the CBCentralManager used to make the connection and any future CBCentralManagers with the same restoration ID will not be able to connect to peripherals. The problem can last for days. To resolve the issue the system bluetooth must be toggled on/off, the app force quit or the restorationID must be changed.


I've posted a radar for the issue here: https://bugreport.apple.com/web/?problemID=33728133


I'd be great to know if anyone else has seen this issue. Also Apple folks will this problem be addressed before the first public release of iOS 11?


I believe these other posts are for the same issue:

https://forums.developer.apple.com/message/244494#244494

https://forums.developer.apple.com/thread/83852

Details

  1. Create a CBCentralManager with a restoration ID (i.e. State preservation enabled)
  2. Attempt to connect to a peripheral. Hold a reference to the peripheral
  3. Upon successful connection: Set the peripheral reference to
    nil
    . Do not call
    cancelPeripheralConnection:
  4. Attempt to connect to the peripheral again

Expected: CBCentralManager.state continues to return CBCentralManagerStatePoweredOn. Connection can successfully be made.

Actual: CBCentralManager.state returns CBCentralManagerStatePoweredOff or CBCentralManagerStateUnknown. Connection cannot be made.

Configuration:

  • OS: iOS 11 - public beta 3
  • Device: iPhone 6s ( Suspected to affect all iOS 11 public beta 2+ devices)

Notes:

  • Toggling bluetooth on/off resolves the issue. Force quitting the app will resolve the issue.
  • Killing the app via the debugger will not resolve the issue. It's suspected that any other non-force quits of the application like termination due to mermory pressure will not resolve the issue.
  • When the peripheral is not cancelled correctly the following is observed:
    2017-08-03 19:00:45.840993-0700 StatePreservationTest[542:298206] [CoreBluetooth] API MISUSE: Cancelling connection for unused peripheral <CBPeripheral: 0x1c01078f0, identifier = YOUR_PERIPHERAL_UUID, name = PeripheralName, state = connecting>, Did you forget to keep a reference to it?
  • The issue is not observed on iOS 10

The problem can be solved by calling

cancelPeripheralConnection:
before the reference is dropped. Ideally
cancelPeripheralConnection:
would always call but under certain circumstances it may difficult to practically do so. This bug is particularly severe because of how long it lasts – seems to last until the bluetooth system is restarted which may be for days for some users – and how badly it hinders the use of CoreBluetooth – cental managers using the affected restoration ID are rendered unusable. Finally even if
cancelPeripheralConnection:
is required for cancelling a single connection, failing to call it should not render the CBCentralManager unusable for other connections.


For details see the code below which will cause the problem.


//DemoViewController.m

#import "DemoViewController.h"
#import <CoreBluetooth/CoreBluetooth.h>

NSString * const kDemoPeripheralUUID = @"YOUR_PERIPHERAL_UUID";


@interface DemoViewController () <CBCentralManagerDelegate>

@property (weak, nonatomic) IBOutlet UITextView *debugTextView;//A textview that shows the debug output
@property (weak, nonatomic) IBOutlet UIButton *findPeripheralButton;//A button that starts a connection to the peripheral when tapped. Its disabled while the connection is being attempted

@property(nonatomic)NSMutableString *mutableLogs;

@property(nonatomic)CBCentralManager *centralManager;
@property(nonatomic)CBPeripheral *peripheral;

@end

@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.mutableLogs = [NSMutableString new];
    [self addLogEvent:@"View Loaded"];
}

#pragma mark - User Actions

- (IBAction)findPeripheralTapped:(id)sender {
    self.findPeripheralButton.enabled = NO;
    [self addLogEvent:@"Find Peripheral Tapped"];
    [self startConnectingToPeripheral];
}

#pragma mark - CoreBluetooth

-(void)startConnectingToPeripheral{

    if (!self.centralManager) {
        self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey:@"MyRestorationID"}];
    }

    if (self.centralManager.state == CBManagerStatePoweredOn) {
        NSArray *allPeripherals = [self.centralManager retrievePeripheralsWithIdentifiers:@[[[NSUUID alloc] initWithUUIDString:kDemoPeripheralUUID]]];
        CBPeripheral *peripheral = allPeripherals.firstObject;
        if (!peripheral) {
            [self addLogEvent:@"Peripheral Not Found in Cache"];//For the sake of brevity in this demo, the peripheral must be in the BLE cache.
            return;
        }

        self.peripheral = peripheral;
        [self.centralManager connectPeripheral:self.peripheral options:nil];
        [self addLogEvent:@"Connecting Peripheral"];
    }
}

-(void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *,id> *)dict{
    //We dont't actually use this method but it must be implemented.
}

-(void)centralManagerDidUpdateState:(CBCentralManager *)central{
    if (central.state == CBManagerStatePoweredOn) {
        [self startConnectingToPeripheral];
    }
}

-(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
    [self addLogEvent:@"Found peripheral"];
    self.findPeripheralButton.enabled = YES;

    //COMMENTING OUT THE BELOW CAUSES THE BUG. If `cancelPeripheralConnection:` is not called, the CBCentralManager and all future CBCentralManagers with the same restoration ID will be broken (CBCentralManagerState will always be CBCentralManagerStateOff).
//    [self.centralManager cancelPeripheralConnection:self.peripheral];

    self.centralManager = nil;
    self.peripheral = nil;
}

#pragma mark - Logging
-(void)addLogEvent:(NSString *)event{
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"HH:mm:ss.SSS"];

    NSString *lineToAdd = [dateFormatter stringFromDate:[NSDate date]];
    lineToAdd = [lineToAdd stringByAppendingString:@"| "];
    lineToAdd = [lineToAdd stringByAppendingString:event];
    [self.mutableLogs appendString:[NSString stringWithFormat:@"%@\n", lineToAdd]];
    self.debugTextView.text = self.mutableLogs;
}

@end

Accepted Reply

Please test again with Beta 5 and update your radar accordingly if the behavior is different.


iOS 11 is in general going to be less forgiving for apps which don't hold a proper reference to CB objects even if the problem described here becomes less severe. So, the best is to manage object life cycles properly.

Replies

Please test again with Beta 5 and update your radar accordingly if the behavior is different.


iOS 11 is in general going to be less forgiving for apps which don't hold a proper reference to CB objects even if the problem described here becomes less severe. So, the best is to manage object life cycles properly.

I had same problem. It seems it was because of iOS bugs in iOS 11-beta 4. Now in iOS 11-beta 5 it's fixed and works fine.

I tested again with Beta 5 and the issue has been resolved. Thanks Gualtier for the response and the CoreBluetooth team for the fix.


When you say "iOS 11 is in general going to be less forgiving for apps which don't hold a proper reference to CB objects", is there anything specific we should test for? Will connection requests be ignored? Will state restoration be less responsive?

When you drop all the references to a CB object in your app, it takes some time for the BT subsystem to catch up to this fact and take necessary actions (drop the connection for instance).


Before, this could take a few seconds. So even if you have dropped your objects by accident, you might still maintain a connection, write some data, and even get a response back without knowing that you have over released your manager, for example. This usually ended up covering any coding mistakes in retaining your objects.


In iOS 11, this reaction is immediate, and you may now see failures of CB actions if you are not maintaining your memory objects properly.

Just make sure everything is retained properly, and not overreleased or released before it's time, and there will be no issues.

Ok that makes sense. Bugs that were being hidden by iOS 10 and earlier's delayed approach to cancelling the connection after all references were dropped may be revealed in iOS 11.


Thanks so much for the detailed answer Gualtier.

I just installed iOS 11.0.3 and am experiencing this issue now.

I wrote two apps (one is a test app that only tests this feature) and confirmed it works on 10.3.3 on 3 different phones but I upgraded the iPhone 6s and it no longer works. I have the test project ready to share, let me know how to get it to you.

This issue is appearing in iOS 11.0.3

This problem still exists in IOS 11.1.1


(tested on iphone 7plus IOS 11.1.0 and ipod 6Gen IOS 11.1.1)

If the BT State is Not Connected (in phone settings) i am getting as powered-off state. Is there any state preservation in iOS 11. By toggling bluetooth on/off resolves the issue.


Is the bug resolved in public release of iOS 11.x?

Same problem