iBeacon是什么?
苹果官方对iBeacon的描述:iBeacon是iOS 7推出的一项技术,可为APP提供新的位置感知功能。利用蓝牙低功耗(BLE),具有iBeacon技术的设备可用于在对象周围建立区域。这允许iOS设备确定它何时进入或离开该区域,以及估计与信标的接近度。在使用iBeacon技术时,需要考虑硬件和软件组件,本文档将介绍这两者,以及建议的用途和最佳实践,以帮助确保高效部署,从而实现出色的用户体验。
采用iBeacon技术的设备
采用iBeacon技术的设备可以使用纽扣电池供电一个月或更长时间,或者使用更大的电池一次运行数月,或者可以在外部长时间供电。iOS设备也可以配置为生成iBeacon广告,但此功能的范围有限。iBeacon广告通过蓝牙低功耗提供以下信息:
UUID,主要和次要值提供iBeacon的标识信息。该信息本质上是分层的,主要和次要字段允许细分由UUID建立的身份。可以使用OS X中的uuidgen命令行实用程序生成UUID,也可以使用NSUUID Foundation类以编程方式生成UUID。
iBeacon软件 - 核心位置API
在iOS 7之前,核心位置使用由地理位置(纬度和经度)和半径定义的区域,称为“地理围栏”。通过定义具有标识符的区域,iBeacon实现了新的灵活性。这允许将信标附加到不绑定到单个位置的对象上。例如,信标设备可用于设置诸如食物卡车或游轮之类的可移动物体周围的区域。此外,多个设备可以使用相同的标识符。这将使零售连锁店能够在其所有位置使用信标,并允许iOS设备知道何时进入其中任何一个。隐私和位置由于iBeacon是Core Location的一部分,因此需要相同的用户授权才能使用。当APP尝试使用iBeacon API时,用户将看到相同的位置授权警报:
在CoreLocation中使用信标区域API的APP将显示在“隐私”>“位置服务”下的“设置”APP中,用户可以随时允许或拒绝APP访问iBeacon功能。此外,任何与iBeacon关联的蓝牙数据包都将从CoreBluetooth API中排除。
与地理围栏区域监控一样,当处于活动状态时,状态栏将显示空心箭头。使用测距时,状态栏将显示实心位置箭头。
iBeacon的准确性
当iOS设备检测到信标的信号时,它使用信号的强度(RSSI或接收信号强度指示)来确定信标的接近度以及其估计接近度的准确性。信号越强,iOS对信标的接近程度就越高。信号越弱,iOS对信标的接近程度越低。
当iOS设备可以清晰地接收GPS信号时,例如当设备处于户外开放状态且轨道GPS卫星的视线畅通时,你的位置就可以更准确地确定。
区域监测
与现有的地理围栏区域监视类似,APP可以在设备进入或离开由信标定义的区域时请求通知。当APP使该请求开始监视信标区域时,它必须指定iBeacon广播包的UUID。虽然APP仅限于20个受监控区域,但通过在多个位置使用单个UUID,设备可以轻松地同时监控多个物理位置。
除了UUID之外,APP还可以选择提供主要和次要字段,以进一步指定要监视的信标区域。假设我们指定一块区域,如果APP仅为信标区域指定UUID,则当进入或离开该区域时将会通知它。由于主要字段用于确定特定区域,如果只是在进入该区域时得到通知,APP可以使用UUID +主要值来配置信标区域。如果只想在进入该区域的指定位置时收到通知,APP可以使用颗粒度更细的值来配置信标,即:UUID + major + minor的组合。
应用实例
1.BLE设备数据同步
BLE设备与APP配对成功之后,为该设备注册IBeacon信标。此时设备通过广播包把数据点传给APP,APP再开启HTTP请求上传数据点到云端。当iPhone与BLE设备断连的时候,数据同步中断。当iPhone再次进入设备蓝牙有效距离内,设备发送IBeacon广播包,此广播包与传输数据点的广播包不同,它的作用是用来唤醒APP,它必须带有特定标识的UUID,当iPhone监听到IBeacon广播包中的UUID,与注册信标的UUID相同时,发起设备扫描并恢复连接状态,同步数据。
IBeacon唤醒设备的能力非常强大,锁屏、后台挂起、kill都能唤醒APP。
代码示例:
// 初始化 Location
- (CLLocationManager *)locationManager {
if (!_locationManager) {
_locationManager = [[CLLocationManager alloc] init];
_locationManager.delegate = self;
_locationManager.desiredAccuracy = kCLLocationAccuracyBest;
_locationManager.distanceFilter = kCLDistanceFilterNone;
// _locationManager.allowsBackgroundLocationUpdates = YES;
_locationManager.pausesLocationUpdatesAutomatically = NO;
}
return _locationManager;
}
// 注册IBeacon监听
- (void)startIbeaconMonitorWithUUID:(NSString *)uuid devId:(NSString *)devId {
if (![PHBleUnlockUtil isValidUUID:uuid]) {
return;
}
if (![self checkIBeacon:devId]) {
return;
}
NSString *ibeaconIdentifer = [PHBleUnlockUtil ibeaconRegionIdentifierWithDevId:devId];
CLBeaconRegion *ibeacon = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:[PHBleUnlockUtil generatedStandardUUIDWithDeviceUUID:uuid]] identifier:ibeaconIdentifer];
ibeacon.notifyOnEntry = YES;
ibeacon.notifyOnExit = YES;
ibeacon.notifyEntryStateOnDisplay = YES;
// 系统框架会根据identifier和Region类型自动替换,不用手动先stop stopMonitoringForRegion停止IBeacon监听。这是异步完成的,可能不会立即反映在MonitoredRegions中
[self.locationManager stopMonitoringForRegion:ibeacon];
[self.locationManager startMonitoringForRegion:ibeacon];
}
// 移除单个IBeacon监听
- (void)stopIbeaconMonitorWithDevId:(NSString *)devId {
NSSet *monitoredRegions = [self.locationManager monitoredRegions];
for (CLRegion *rg in monitoredRegions) {
if ([rg isMemberOfClass:[CLBeaconRegion class]] && [rg.identifier containsString:devId]) {
[self.locationManager stopMonitoringForRegion:rg];
break;
}
}
}
// check 当前设备是否已注册过IBeacon
- (BOOL)checkIBeacon:(NSString *)devId {
NSSet *regions = [self.locationManager monitoredRegions];
// 系统允许IBeacon注册上线为20个,达到20个批量移除,再添加
if (regions.count == kBleIBeaconMonitorMaxCount) {
[self removeIbeaconAllMonitors];
return [self checkIBeacon:devId];
} else {
for (CLRegion *rg in regions) {
if ([rg isMemberOfClass:[CLBeaconRegion class]]) {
NSString *identifier = rg.identifier;
// 该设备已经注册过IBeacon,不再注册
if ([identifier containsString:L(devId)]) {
return NO;
}
}
}
return YES;
}
}
// 移除所有IBeacon监听
- (void)removeIbeaconAllMonitors {
NSSet *regions = [self.locationManager monitoredRegions];
for (CLRegion *rg in regions) {
if ([rg isMemberOfClass:[CLBeaconRegion class]]) {
[self.locationManager stopMonitoringForRegion:rg];
}
}
}
// 发起设备连接
- (void)repeatBleStartListening:(NSString *)devId {
BOOL connectedCompleted = [self bleConnectedCompletedStatusWithDevId:devId];
if (!connectedCompleted) {
NSLog(@"---ibeacon----50秒后蓝牙重连");
// 设备重连
[self removeBleConnectedCompletedStatusWithDevId:devId];
}
}
// 设置当前设备的状态为连接中
- (void)setupBleConnectedCompletedWithDevId:(NSString *)devId {
[self.bleConnectedCompletedList setObject:@(YES) forKey:devId];
}
// 获取当前设备的连接状态
- (BOOL)bleConnectedCompletedStatusWithDevId:(NSString *)devId {
return [[self.bleConnectedCompletedList valueForKey:devId] boolValue];
}
// 移除当前设备的连接状态
- (void)removeBleConnectedCompletedStatusWithDevId:(NSString *)devId {
return [self.bleConnectedCompletedList removeObjectForKey:devId];
}
// 重置设备连接状态
- (void)resetStatesWithIdentifier:(NSString *)identifier {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.handleBeaconList setValue:@(NO) forKey:identifier];
self.needRequestStateForRegion = YES;
});
}
#pragma mark - backgroup task
- (void)beginBackgroundUpdateTask {
weakify(self);
self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
strongify(self);
[self endBackgroundUpdateTask];
}];
}
- (void)endBackgroundUpdateTask {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#pragma mark - CLLocationManagerDelegate
// notifyOnEntry:缺省为NO。设置为YES时,当用户进入一个iBeaconRegion内,无论此应用处于什么状态,调用此委托方法
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
if ([region isMemberOfClass:[CLBeaconRegion class]]) {
NSLog(@"---ibeacon----进入IBeacon区域-----%@", region.identifier);
}
}
// notifyOnExit:缺省为NO。设置为YES时,当用户进入一个iBeaconRegion内,无论此应用处于什么状态,调用此委托方法
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
if ([region isMemberOfClass:[CLBeaconRegion class]]) {
NSLog(@"---ibeacon----离开IBeacon区域-----%@", region.identifier);
}
}
- (void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region {
if ([region isMemberOfClass:[CLBeaconRegion class]]) {
NSLog(@"---ibeacon----发起监听成功-----%@", region.identifier);
}
}
- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(nullable CLRegion *)region withError:(NSError *)error {
if ([region isMemberOfClass:[CLBeaconRegion class]]) {
NSLog(@"---ibeacon----监听失败-----%@", region.identifier);
}
}
// notifyEntryStateOnDisplay:缺省为NO。设置为YES时,当屏幕点亮且在一个iBeaconRegion内,无论此应用处于什么状态,调用此委托方法
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region {
if (![region isMemberOfClass:[CLBeaconRegion class]]) {
return;
}
NSLog(@"---ibeacon----触发-----%ld--%@", (long)state, region.identifier);
NSString *identifier = region.identifier;
NSArray *identifierInfos = [region.identifier componentsSeparatedByString:@"_"];
if (identifierInfos.count != kBleUnlockReginIdentifierElementCount) {
return;
}
switch (state) {
case CLRegionStateInside: {
BOOL isHandling = [[self.handleBeaconList objectForKey:identifier] boolValue];
if (isHandling) {
return;
}
[self.handleBeaconList setValue:@(YES) forKey:identifier];
// NSString *pid = [identifierInfos objectAtIndex:0];
NSString *devId = [identifierInfos objectAtIndex:1];
// NSString *uid = [identifierInfos objectAtIndex:3];
NSLog(@"will find device %@", devId);
NSTimeInterval limitSeconds = [NSDate dateWithTimeIntervalSinceNow:5].timeIntervalSince1970;
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:2]];
} while (![SmartDevice deviceWithDeviceId:devId] && ([NSDate date].timeIntervalSince1970 < limitSeconds));
self.deviceModel = [SmartDevice deviceWithDeviceId:devId].deviceModel;
if (!self.deviceModel) {
NSLog(@"not find device %@", devId);
[self resetStatesWithIdentifier:identifier];
return;
}
NSLog(@"did find device %@", self.deviceModel.devId);
// devId一致、设备不在线才可以发起连接蓝牙
if ([self.deviceModel.devId isEqualToString:devId] && !self.deviceModel.isOnline) {
// 查找到设备才开启蓝牙
[self addBleConnectedStateObserver];
[self bleStartListening];
// 50秒后如果蓝牙没连上重连一次
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(50 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self repeatBleStartListening:devId];
});
}
[self resetStatesWithIdentifier:identifier];
}
case CLRegionStateOutside:
case CLRegionStateUnknown:
if (_needRequestStateForRegion) {
if (_requestStateForRegionCount < MAX_RETRY_COUNT) {
_requestStateForRegionCount++;
[self.locationManager requestStateForRegion:region];
} else {
_needRequestStateForRegion = NO;
_requestStateForRegionCount = 0;
}
}
break;
default:
break;
}
}
- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
}
2.解锁
家里面的门锁,汽车的门锁,如果硬件的模组支持IBeacon,iPhone进入门锁/汽车蓝牙的范围,门锁发送IBeacon广播包,唤醒APP并发起重连,重连成功之后APP下发指令来解锁,解锁成功之后需要马上移除IBeacon监听,防止重复开锁。
踩坑点
IBeacon是Core Location框架提供的一个能力,那就必须开启位置权限,并且位置权限为始终允许,精确位置也要打开。否则APP在后台、kill进程的时候,是没法通过IBeacon唤醒APP的。除了定位权限,后台蓝牙通讯的权限也必须开启,如果你的业务场景是:手机作为蓝牙中心 server,主动发现蓝牙设备,建立连接,后台不断刷新和保持蓝牙通信会话,那么需要开启Uses Bluetooth LE accessories。如果你的业务场景是:把手机模拟成蓝牙设备,被迫连接,那么需要开启Acts as a Bluetooth LE accessory。