iOS iBeacon /蓝牙连接当应用程序死机时

Posted

技术标签:

【中文标题】iOS iBeacon /蓝牙连接当应用程序死机时【英文标题】:iOS iBeacon / Bluetooth connectivity when app is dead and gone 【发布时间】:2017-07-17 13:32:01 【问题描述】:

我需要什么:

当应用程序死机且设备已插入或在附近时,启动 iBeacon 委托方法(例如 didDetermineStatedidRangeBeaconsdidEnterRegiondidExitRegion)的可预测、可靠且可靠的方式。

现状

我正在为父母制作一个应用程序,供他们的孩子使用,以帮助他们在重要时刻关闭手机。该应用采用 Objective-C 语言,即使在应用生命周期之后,它也需要保持与蓝牙设备的持久连接。

我已经尝试了很长时间才能让它发挥作用,并且我得到了很多 S.O. 的帮助。海报,目前我知道我必须在我的设备中使用 iBeacon 才能从终止状态启动(这是我使用它的唯一原因,如果有另一种方法可以从终止状态启动应用程序,我会很乐意转储它)。为了澄清,我需要在同一设备(我已经构建)iBeacon 和可靠的 BT 连接中使用 2 件东西。我需要此设备连接配对,因为这是从 BT 设备发送/接收命令的唯一方法。我发现在后台触发的didRangedidEnter 委托方法充其量是不可靠的。他们并不总是立即开火,他们只开火几次,整个事情就死了(我现在知道这 10 秒的窗口是终止应用程序的预期行为)。我什至整天都在不停地插拔它,寻找应用程序恢复生机的任何迹象,但什么也没发生......

当应用程序打开时,一切正常,但是当应用程序靠近我的信标/蓝牙时,我希望它在应用程序内启动一种临时锁定屏幕。当应用程序在前台时,我已经很好地完成了这部分。如果一个孩子试图关闭应用程序或后台,我想通过让我的 BT 设备在它终止后启动到后台来响应(我知道 UI 不会出现,这很好,我只需要启动一系列功能) .然后它将连接到蓝牙并从设备接收一些命令。听起来很简单吧?事情变得一团糟。

一些背景:我在 info.plist 中为蓝牙和信标添加了所有背景模式,当应用程序在前台时一切正常...

如果在范围内检测到 iBeacon,然后我想使用 10 秒的窗口通过 BT 配对连接到我的盒子并通过命令发送。到目前为止,这很不稳定...... iBeacon 测距功能不会在应用程序终止时触发,它们只会在最奇怪的用例上触发。我似乎无法预测他们什么时候开火。


我的代码

ibeaconManager.h

@interface IbeaconManager : NSObject

@property (nonatomic) BOOL waitingForDeviceCommand;
@property (nonatomic, strong) NSTimer *deviceCommandTimer;

+ (IbeaconManager *) sharedInstance;
- (void)startMonitoring;
- (void)stopMonitoring;
- (void)timedLock:(NSTimer *)timer;

@end

ibeaconManager.m

@interface IbeaconManager () <CLLocationManagerDelegate>

@property (nonatomic, strong) BluetoothMgr *btManager;
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) CLBeaconRegion *region;
@property (nonatomic) BOOL connectedToDevice;

@end

NSString *const PROXMITY_UUID = @"00000000-1111-2222-3333-AAAAAAAAAAAA";
NSString *const BEACON_REGION = @"MY_CUSTOM_REGION";

const int REGION_MINOR = 0;
const int REGION_MAJOR = 0;



@implementation IbeaconManager
+ (IbeaconManager *) sharedInstance 
    static IbeaconManager *_sharedInstance = nil;
    static dispatch_once_t oncePredicate;

    dispatch_once(&oncePredicate, ^
        _sharedInstance = [[IbeaconManager alloc] init];
    );

    return _sharedInstance;




- (id)init 
    self = [super init];

    if(self) 
        self.locationManager = [[CLLocationManager alloc] init];
        self.locationManager.delegate = self;
        [self.locationManager requestAlwaysAuthorization];
        self.connectedToDevice = NO;
        self.waitingForDeviceCommand = NO;

        self.region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:PROXMITY_UUID]
                                                              major:REGION_MAJOR
                                                              minor:REGION_MINOR
                                                         identifier:BEACON_REGION];

        self.region.notifyEntryStateOnDisplay = YES;
        self.region.notifyOnEntry = YES;
        self.region.notifyOnExit = YES;
    

    return self;



- (void)startMonitoring 
    if(self.region != nil) 
        NSLog(@"**** started monitoring with beacon region **** : %@", self.region);

        [self.locationManager startMonitoringForRegion:self.region];
        [self.locationManager startRangingBeaconsInRegion:self.region];
    



- (void)stopMonitoring 
    NSLog(@"*** stopMonitoring");

    if(self.region != nil) 
        [self.locationManager stopMonitoringForRegion:self.region];
        [self.locationManager stopRangingBeaconsInRegion:self.region];
    



- (void)triggerCustomLocalNotification:(NSString *)alertBody 
    UILocalNotification *localNotification = [[UILocalNotification alloc] init];
    localNotification.alertBody = alertBody;
    [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];





#pragma mark - CLLocationManager delegate methods

- (void)locationManager:(CLLocationManager *)manager
      didDetermineState:(CLRegionState)state
              forRegion:(CLRegion *)region 

    NSLog(@"did determine state STATE: %ld", (long)state);
    NSLog(@"did determine state region: %@", region);

    [self triggerCustomLocalNotification:@"made it into the did determine state method"];

    NSUInteger appState = [[UIApplication sharedApplication] applicationState];
    NSLog(@"application's current state: %ld", (long)appState);

    if(appState == UIApplicationStateBackground || appState == UIApplicationStateInactive) 
        NSString *notificationText = @"Did range beacons... The app is";
        NSString *notificationStateText = (appState == UIApplicationStateInactive) ? @"inactive" : @"backgrounded";
        NSString *notificationString = [NSString stringWithFormat:@"%@ %@", notificationText, notificationStateText];

        NSUserDefaults *userDefaults = [[NSUserDefaults alloc] init];
        bool isAppLockScreenShowing = [userDefaults boolForKey:@"isAppLockScreenShowing"];

        if(!isAppLockScreenShowing && !self.waitingForDeviceCommand) 
            self.waitingForDeviceCommand = YES;

            self.deviceCommandTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
                                                                       target:self
                                                                     selector:@selector(timedLock:)
                                                                     userInfo:notificationString
                                                                      repeats:NO];
        

     else if(appState == UIApplicationStateActive) 

        if(region != nil) 
            if(state == CLRegionStateInside) 
                NSLog(@"locationManager didDetermineState INSIDE for %@", region.identifier);
                [self triggerCustomLocalNotification:@"locationManager didDetermineState INSIDE"];

             else if(state == CLRegionStateOutside) 
                NSLog(@"locationManager didDetermineState OUTSIDE for %@", region.identifier);
                [self triggerCustomLocalNotification:@"locationManager didDetermineState OUTSIDE"];

             else 
                NSLog(@"locationManager didDetermineState OTHER for %@", region.identifier);
            
        

        //Upon re-entry, remove timer
        if(self.deviceCommandTimer != nil) 
            [self.deviceCommandTimer invalidate];
            self.deviceCommandTimer = nil;
        
    



- (void)locationManager:(CLLocationManager *)manager
        didRangeBeacons:(NSArray *)beacons
               inRegion:(CLBeaconRegion *)region 

    NSLog(@"Did range some beacons");

    NSUInteger state = [[UIApplication sharedApplication] applicationState];
    NSString *notificationStateText = (state == UIApplicationStateInactive) ? @"inactive" : @"backgrounded";
    NSLog(@"application's current state: %ld", (long)state);

    [self triggerCustomLocalNotification:[NSString stringWithFormat:@"ranged beacons, application's current state: %@", notificationStateText]];

    if(state == UIApplicationStateBackground || state == UIApplicationStateInactive) 
        NSString *notificationText = @"Did range beacons... The app is";
        NSString *notificationString = [NSString stringWithFormat:@"%@ %@", notificationText, notificationStateText];

        NSUserDefaults *userDefaults = [[NSUserDefaults alloc] init];
        bool isAppLockScreenShowing = [userDefaults boolForKey:@"isAppLockScreenShowing"];

        if(!isAppLockScreenShowing && !self.waitingForDeviceCommand) 
            self.waitingForDeviceCommand = YES;

            self.deviceCommandTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
                                                                       target:self
                                                                     selector:@selector(timedLock:)
                                                                     userInfo:notificationString
                                                                      repeats:NO];
        

     else if(state == UIApplicationStateActive) 
        if(self.deviceCommandTimer != nil) 
            [self.deviceCommandTimer invalidate];
            self.deviceCommandTimer = nil;
        
    



- (void)timedLock:(NSTimer *)timer 
    self.btManager = [BluetoothMgr sharedInstance];

    [self.btManager sendCodeToBTDevice:@"magiccommand"
                        characteristic:self.btManager.lockCharacteristic];

    [self triggerCustomLocalNotification:[timer userInfo]];

    self.waitingForDeviceCommand = NO;



- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region 
    NSLog(@"Did Enter Region: %@", region);
    [self triggerCustomLocalNotification:[NSString stringWithFormat:@"Did enter region: %@", region.identifier]];



- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region 
    NSLog(@"Did Exit Region: %@", region);
    [self triggerCustomLocalNotification:[NSString stringWithFormat:@"Did exit region: %@", region.identifier]];

    //Upon exit, remove timer
    if(self.deviceCommandTimer != nil) 
        [self.deviceCommandTimer invalidate];
        self.deviceCommandTimer = nil;
    



- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error 
    NSLog(@"monitoringDidFailForRegion EPIC FAIL for region %@ withError %@", region.identifier, error.localizedDescription);





@end

【问题讨论】:

【参考方案1】:

我已经为 ios 构建了一个类似的系统,它使用 iBeacon 传输在后台唤醒,然后连接到蓝牙 LE 以交换数据。 请放心,这一切皆有可能,只是开始工作很棘手,调试起来也更棘手。

使用蓝牙 LE 连接的一些提示:

当应用程序被终止时,Beacon 测距功能不会触发除非您还监视信标并获得 didEnterdidExit 转换,这将重新启动应用程序按照你的描述进入背景 10 秒。同样,这只会在您从区域内过渡到区域外时发生,反之亦然。这很难测试,因为当您终止应用程序时,您可能没有意识到 CoreLocation 认为您在“区域内”,但您不会收到用于检测信标的唤醒事件。

为了在后台获取蓝牙事件,您需要确保您的 Info.plist 声明了这一点:

<key>UIBackgroundModes</key>
<array>
   <string>bluetooth-central</string>
</array>

如果不存在,您绝对不会在后台收到对 didDiscoverPeripheral 的回调。

您需要在应用启动时开始扫描蓝牙,并在收到func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber)的回调时连接

保存上面的 peripheral 实例的副本,因为您只会在后台获得 一个 回调,以便从每个唯一的蓝牙设备进行发现。如果连接失败,您可以使用相同的 peripheral 对象实例重试。

为了调试从终止状态重新启动,我添加了许多 NSLog 语句(我添加了在代码中打开和关闭它们的能力),然后在 XCode 的 Windows 中查找这些 ->设备 -> 我的 iPhone 面板,您可以在其中展开屏幕底部的小箭头以显示设备上所有应用程序的日志。如果您的应用从终止状态重新启动,您绝对会在此处看到日志。

【讨论】:

很高兴你能成功。如果您可以就对您产生影响的原因提出任何意见,请发表评论,因为它可能会帮助其他有类似问题的人。 好吧,也许前几天我说它工作正常,这更像是侥幸。今天我已经花了 4 个多小时插拔设备,但我没有收到任何以下方法的日志或通知:didDetermineStatedidRangeBeaconsdidEnterRegiondidExitRegionmonitoringDidFailForRegion跨度> 不知道如何处理这件事。当我后台应用程序时,应用程序检测 iBeacon 没有问题,didDetermineState 方法几乎每秒触发一次,这是非常可靠的。当我终止应用程序时,我需要非常可靠地检测到某种 iBeacon 方法。此外,当应用程序已终止数天或第二次启动时,我也需要相同的内容。 用新信息更新了我的原始问题并添加了我的代码以便更容易帮助我 你并不孤单。测试是艰难的。如果您没有收到监控触发器,我怀疑您可能在该区域“内部”。在后台传输到“外部”可能非常缓慢——需要 15-30 分钟,因为它们依赖于操作系统上运行的其他组件的机会性扫描。我会将应用程序带到前台,确认您不在区域内,然后将应用程序置于后台(或将其杀死),然后打开信标以尝试触发。另一个提示:如果您重新启动手机,不要指望 5 分钟内有任何触发器。 CoreLocation 没有在 iOS 启动时快速完全启动。

以上是关于iOS iBeacon /蓝牙连接当应用程序死机时的主要内容,如果未能解决你的问题,请参考以下文章

如何自动连接 CBPeripheral(带有 iBeacon + 蓝牙芯片的 BLE 设备)并且应用程序未运行

在蓝牙重置之前,蓝牙设备在测距和监控 iBeacons 后无法连接

Android IBeacon

后台模式下的 iBeacon 访问

iOS 中 iBeacon 开发

将 iOS 应用重新连接到配对的蓝牙设备?