iOS地理围栏技术的应用
遇到一个需求,要求监测若干区域,设备进入这些区域则要上传数据,且可以后台监测,甚至app被杀死也要监测。发现oc的地理围栏技术完美匹配这个需求,任务做完了,把遇到的坑记录下来,也许能帮到你呢。
要做这个需求,我们需要把任务分成两大块,一块是支持后台监测且app被杀掉也要持续监测,另一块是如何进行区域监测。
而区域监测我们有3种方法完成:
1,oc自有的,利用CLLocationManager监测若干CLCircularRegion区域
2,高德地图旧版地理围栏,利用AMapLocationManager监测若干AMapLocationCircleRegion区域。其实是对CLLocationManager进行简单封装,用法也和CLLocationManager基本一致
3,高德地图新版地理围栏,有个专门进行区域监测的管理类AMapGeoFenceManager,该方法对区域监测做了很多优化。
当围栏创建完毕,且围栏创建成功时会启动定位,这部分无需您来设置,SDK内部执行。 定位机制:通过“远离围栏时逐渐降低定位频率”来降低电量消耗,“离近围栏时逐渐提高定位频率”来保证有足够的定位精度从而完成围栏位置检测。需要注意,在iOS9及之后版本的系统中,如果您希望程序在后台持续检测围栏触发行为,需要保证manager的allowsBackgroundLocationUpdates为YES,设置为YES的时候必须保证 Background Modes 中的 Location updates 处于选中状态,否则会抛出异常。
一 如何实现后台定位且被杀掉也能持续定位
1 实现后台定位
1.1 工程配置
- iOS8之前
如果想要定位需要在plist文件中位置keyPrivacy - Location Usage Description
,默认只在前台定位
,如果想开启后台定位需要在开启后台模式
- iOS8
需要在plist文件中配置NSLocationWhenInUseUsageDescription(前台定位)
NSLocationAlwaysUsageDescription(前后台定位) 注:可以两个都配置上
1.2 用户权限请求(代码实现)
利用CLLocationManager的实例去请求权限,如果使用的是高德地图,就用AMapLocationManager或者AMapGeoFenceManager的实例去请求。需要注意的是,不管使用哪一个类,只要有一个去请求权限就可以了。
//请求前台定位权限 - (void)requestWhenInUseAuthorization __OSX_AVAILABLE_STARTING(__MAC_NA, __IPHONE_8_0); //请求前后台定位权限,即使当前权限为前台定位也会生效 - (void)requestAlwaysAuthorization __OSX_AVAILABLE_STARTING(__MAC_NA, __IPHONE_8_0);
-
注意:如果是前台定位权限,但是开始了后台模式,在后台也是可以定位的,但是屏幕的上边会有蓝条,提示用户是哪个应用在定位
-
iOS 9
如果想要在后台定位,除了配置NSLocationAlwaysUsageDescription(前后台定位)
外,还需要手动设置allowsBackgroundLocationUpdates = YES
-
指定定位是否会被系统自动暂停属性也要设置为NO。pausesLocationUpdatesAutomatically = NO;
2 实现app被杀掉也能定位
如果你申请了后台定位权限且用户同意,那么当你的定位请求被触发的时候,比如位置移动1000米重新定位,系统会自动唤醒你的app,在application:didFinishLaunchingWithOptions方法中,
UIApplicationLaunchOptionsLocationKey
就是被定位唤醒
在被唤醒后一定要创建你的定位或监测的对象。这样才能响应到定位监测的回调。在我的例子里,self.regionManager是一个单例,只要app启动,就会创建并且开始检测,这一步至关重要,是实现app被杀掉也能定位的最关键步骤。
你在被唤醒的这段时间可以做一些简单操作,可以保存定位信息,上传监测数据等。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self locationTest:launchOptions]; return YES; } - (void)locationTest:(NSDictionary *)launchOptions { [self.regionManager starMonitorRegion]; if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) { [self.regionManager saveMessage:@"被区域监测唤醒"]; } } - (RegionManager *)regionManager { return [RegionManager shareInstance]; }
以上就是如何实现后台定位且被杀掉也能定位。接下来我们讨论如何进行区域监测。
二 区域监测,也称地理围栏,或者临近警告
如果希望iOS设备进出某个区域发出通知,那么这种区域监测的功能也被称为临近警告。所谓临近警告的示意图如图所示。
临近警告的示意图
1 oc自有的地理围栏实现
利用CoreLocation就可以实现地理围栏,
1.1 创建CLLocationManager对象,该对象负责获取定位相关信息,并为该对象设置一些必要的属性。
-(CLLocationManager *)locationM { if (!_locationM) { _locationM = [[CLLocationManager alloc] init]; _locationM.delegate = self; _locationM.desiredAccuracy = kCLLocationAccuracyBest; _locationM.distanceFilter = 10; // 主动请求定位授权 #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 [_locationM requestAlwaysAuthorization]; #endif //这是iOS9中针对后台定位推出的新属性 不设置的话 可是会出现顶部蓝条的哦(类似热点连接) #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 [_locationM setAllowsBackgroundLocationUpdates:YES]; #endif _locationM.pausesLocationUpdatesAutomatically = NO; } return _locationM; }
1.2 为CLLocationManager指定delegate属性,该属性值必须是一个实现CLLocationManagerDelegate协议的对象,实现CLLocationManagerDelegate协议的对象.实现CLLocationManagerDelegate协议时可根据需要实现协议中特定的方法.
// 进入指定区域以后将弹出提示框提示用户 -(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region { NSString *message; for (StudentInfoModel *student in self.studentArray) { if ([region.identifier isEqualToString:student.qingqingUserId]) { message = [NSString stringWithFormat:@"进入轻轻家教第%d中心区域",[student.qingqingUserId intValue]/1000+1]; } } [[[UIAlertView alloc] initWithTitle:@"区域检测提示" message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; } // 离开指定区域以后将弹出提示框提示用户 -(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region { NSString *message; for (StudentInfoModel *student in self.studentArray) { if ([region.identifier isEqualToString:student.qingqingUserId]) { message = [NSString stringWithFormat:@"离开轻轻家教第%d中心区域",[student.qingqingUserId intValue]/1000+1]; } } [[[UIAlertView alloc] initWithTitle:@"区域检测提示" message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; } /** * 监听区域失败时调用 * * @param manager 位置管理者 * @param region 区域 * @param error 错误 */ -(void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error { NSLog(@"监听区域失败"); }
1.3调用CLLocationManager的startMonitoringForRegion:方法进行区域监测.区域监测结束时,可调用stopMonitoringForRegion:方法结束区域监测.可以监测多个区域,数量没有上线,不过要考虑性能。
- (void)starMonitorRegion { //监听最近联系的20个家长 测试数据 for (int i = 0; i < 20 ; i++) { StudentInfoModel *student = [[StudentInfoModel alloc] init]; CLLocationCoordinate2D companyCenter; companyCenter.latitude = 31.200546; companyCenter.longitude = 121.599263 + i*0.005; student.location = companyCenter; student.qingqingUserId = [NSString stringWithFormat:@"%d",i*1000]; [self.studentArray addObject:student]; [self regionObserve:student]; } } - (void)regionObserve:(StudentInfoModel *)student { if([CLLocationManager locationServicesEnabled]) { // 定义一个CLLocationCoordinate2D作为区域的圆 // 使用CLCircularRegion创建一个圆形区域, // 确定区域半径 CLLocationDistance radius = 200; // 使用前必须判定当前的监听区域半径是否大于最大可被监听的区域半径 if(radius > self.locationM.maximumRegionMonitoringDistance) { radius = self.locationM.maximumRegionMonitoringDistance; } CLRegion* fkit = [[CLCircularRegion alloc] initWithCenter:student.location radius:radius identifier:student.qingqingUserId]; // 开始监听fkit区域 [self.locationM startMonitoringForRegion:fkit]; // 请求区域状态(如果发生了进入或者离开区域的动作也会调用对应的代理方法) [self.locationM requestStateForRegion:fkit]; } else { // 使用警告框提醒用户 [[[UIAlertView alloc] initWithTitle:@"提醒" message:@"您的设备不支持定位" delegate:self cancelButtonTitle:@"确定" otherButtonTitles: nil] show]; } }
2 高德地图的旧版地理围栏
旧版地理围栏和oc自有的用法基本一致,这里就不累赘。CLLocationManager换成AMapLocationManager,CLCircularRegion换成AMapLocationCircleRegion。文章结尾会有demo下载。
3 高德地图的新版地理围栏
新版的高德地图对地理围栏进行了优化,把地理围栏从AMapLocationManager中剥离,有了自己单独的管理类AMapGeoFenceManager。当围栏创建完毕,且围栏创建成功时会启动定位,这部分无需您来设置,SDK内部执行。 定位机制:通过“远离围栏时逐渐降低定位频率”来降低电量消耗,“离近围栏时逐渐提高定位频率”来保证有足够的定位精度从而完成围栏位置检测。需要注意,在iOS9及之后版本的系统中,如果您希望程序在后台持续检测围栏触发行为,需要保证manager的allowsBackgroundLocationUpdates为YES,设置为YES的时候必须保证 Background Modes 中的 Location updates 处于选中状态,否则会抛出异常。
- AMapGeoFenceManager创建,并设置相关属性
- (AMapGeoFenceManager *)geoFenceManager { if (!_geoFenceManager) { _geoFenceManager = [[AMapGeoFenceManager alloc] init]; _geoFenceManager.delegate = self; _geoFenceManager.activeAction = AMapGeoFenceActiveActionInside | AMapGeoFenceActiveActionOutside; //设置希望侦测的围栏触发行为,默认是侦测用户进入围栏的行为,即AMapGeoFenceActiveActionInside,这边设置为进入,离开触发回调 _geoFenceManager.allowsBackgroundLocationUpdates = YES; //允许后台定位 _geoFenceManager.pausesLocationUpdatesAutomatically = NO; } return _geoFenceManager; }
- 监听区域,可以多个
// 清除上一次创建的围栏 - (void)doClear { [self.geoFenceManager removeAllGeoFenceRegions]; //移除所有已经添加的围栏,如果有正在请求的围栏也会丢弃 } - (void)starMonitorRegions{ if([CLLocationManager locationServicesEnabled]) {
[self doClear]; //监听最近联系的20个家长 测试数据 for (int i = 0; i < kObserveStudentNum ; i++) { StudentInfoModel *student = [[StudentInfoModel alloc] init]; CLLocationCoordinate2D companyCenter; companyCenter.latitude = 31.200546; companyCenter.longitude = 121.599263 + i*0.005; student.location = companyCenter; student.qingqingUserId = [NSString stringWithFormat:@"%d",i*1000]; [self monitorRegion:student]; } } else { DDLogInfo(@"定位服务.定位不可用"); #if DEBUG [QQThemeAlertView showWithTitle:@"定位不能用了" message:@"设备不支持定位,找开发!" options:@[@"我知道了"] block:nil]; #endif } } - (void)monitorRegion:(StudentInfoModel *)studentInfo { // 确定区域半径和圆心 CLLocationCoordinate2D regionCenter; regionCenter.latitude = studentInfo.latitude; regionCenter.longitude = studentInfo.longitude; //该区域的唯一标识 NSString *customID = [NSString stringWithFormat:@"%lf+%lf",studentInfo.latitude,studentInfo.longitude]; [self.geoFenceManager addCircleRegionForMonitoringWithCenter:regionCenter radius:kRegionRadius customID:customID]; } -
实现manager的delegate方法
#pragma mark - AMapGeoFenceManagerDelegate //添加地理围栏完成后的回调,成功与失败都会调用 - (void)amapGeoFenceManager:(AMapGeoFenceManager *)manager didAddRegionForMonitoringFinished:(NSArray<AMapGeoFenceRegion *> *)regions customID:(NSString *)customID error:(NSError *)error { if (error) { DDLogInfo(@"区域检测.添加地理围栏失败%@",error); } else { AMapGeoFenceRegion *region = [regions firstObject]; DDLogInfo(@"区域检测.添加地理围栏成功%@",region.customID); //[self saveMessage:[NSString stringWithFormat:@"区域检测.添加地理围栏成功%@",region.customID]]; } } //地理围栏状态改变时回调,当围栏状态的值发生改变,定位失败都会调用 - (void)amapGeoFenceManager:(AMapGeoFenceManager *)manager didGeoFencesStatusChangedForRegion:(AMapGeoFenceRegion *)region customID:(NSString *)customID error:(NSError *)error { if (error) { DDLogInfo(@"区域检测.定位失败%@",error); }else{ DDLogInfo(@"区域检测.状态改变%@",[region description]); [self saveMessage:[NSString stringWithFormat:@"区域检测.状态改变%ld",[region fenceStatus]]]; switch (region.fenceStatus) { case AMapGeoFenceRegionStatusInside: { //在区域内 } break; case AMapGeoFenceRegionStatusOutside: { //在区域外 } break; case AMapGeoFenceRegionStatusStayed: { //停留超过十分钟 } break; default: { //未知 } break; } } }
-
到此,地理围栏技术讲解结束,遇到的坑:
1 新版地理围栏,高德文档写区域监测半径大于0即可,然而我用模拟器测试,跑gpx文件模拟路线,大于250m才有回调,自己修改模拟器customLocation位置,大于500m才有回调,目前位置还没有搞明白。有遇到同样问题欢迎讨论。
2 要实现app被杀死持续监测区域,一定要知道当你进入监测区域,系统会唤醒app,在application:didFinishLaunchingWithOptions方法中要有处理定位回调的实例。不然只能实现后台监测。
DEMO下载:
1 利用oc自有CLLocationManager监测区域DEMO:https://github.com/wangdachui/monitorRegion
2 高德地图的地理围栏,官方DEMO已经写得很详细,注释也很清除。看了高德的注释也让我明白了CLLocationManager监测区域的实现,赞一个。DEMO地址:http://lbs.amap.com/api/ios-location-sdk/download/