最近在优化项目中的定位功能,总结一下和后台持续定位有关的各方面细节,会涉及到后台运行、延迟定位、无感知唤醒等内容。目前测试的效果是后台挂机(也就是手机关闭屏幕待机)状态下超过 24 小时不间断获取位置并上报服务器。
权限#
首先来看一下权限问题,要使用后台定位首先需要保证 Target -> Capabilities -> Background Modes 的开关打开,并勾选 Location updates ,然后在 info.plist 中添加必要的 NSLocationWhenInUseUsageDescription 和 NSLocationAlwaysAndWhenInUseUsageDescription 描述字段,兼容 iOS 10 以及之前版本的还需要 NSLocationAlwaysUsageDescription ,具体描述内容要求简洁清晰地解释 APP 需要定位的理由以及用途。
这里说一下 iOS 中关于定位的两种权限 使用应用期间 以及 始终 的区别。首先并不是说选择了 使用应用期间 的 APP 就不能够使用后台定位权限了,选择了 使用应用期间 的 APP 依然能够实现后台定位,只是与选择 始终 的情况下在 APP 被杀或者被系统回收之后有一些区别,具体的情况是这样的:
在项目配置正确且代码中 开启了后台定位功能 (具体下文会讲到)的前提下
选择
使用应用期间的 APP 进入后台之后,状态栏会出现蓝色提示横条,显示“APP 名称”正在活跃使用您的位置信息并闪烁,刘海屏设备在左上角定位图标处显示蓝框,直到 APP 回到前台或者被杀掉(包括手动或被动)才消失,代码中无法设置蓝框是否显示;选择
始终的 APP 进入后台之后状态栏无蓝色提示横条,但代码中可以设置是否显示。
两种情况下 APP 均能在后台持续定位不被挂起,除非 APP 被杀死。而被杀死之后选择 始终 的 APP 会在手机移动至少500米之后被唤醒重新定位,而选择 使用应用期间 的 APP 被杀就不再上报了。
这是关于权限需要在项目中做的配置,除此以外,要使用后台定位还需要在提审的时候注意以下几点:
在提审的备注中,向审核人员清晰阐述 APP 需要使用后台定位的原因,并且详细描述 APP 从界面中的什么入口触发开启后台定位,从什么入口能停止后台定位,必要的时候苹果可能还会要求提供这一过程的操作录像,可以事先准备好,上传到国内视频网站提供链接给苹果就行。
在 APP 的上架页面的描述信息中,向用户清晰阐述 APP 的哪些特性和功能必须要使用后台定位权限,并且说明这个操作会影响电池电量。下面是大厂地图应用的描述,可以参考:
百度地图:
【温馨提示】
语音导航和电子狗会持续使用GPS定位服务,切换至后台播报时,仍会保持GPS连接,相比其他操作会消耗更多的电量。高德地图:
【温馨提示】
—高德地图的驾车语音导航、公交换乘提醒和步行语音导航会持续使用GPS定位服务,切换至后台播放时仍会继续,相比其他操作会消耗更多的电量,并影响电池续航时间;腾讯地图:
注意事项:
-在后台持续使用GPS会急剧减少电池续航时长
代码#
创建一个 CLLocationManager 并配置属性:
#import <CoreLocation/CoreLocation.h>
// ...
_locationManager = [CLLocationManager new];
_locationManager.delegate = self;
_locationManager.desiredAccuracy = kCLLocationAccuracyBest;
_locationManager.distanceFilter = 5.0f;
// 申请始终定位权限
[_locationManager requestAlwaysAuthorization];
// 或者申请使用应用期间权限
// [_locationManager requestWhenInUseAuthorization];
// 最后需要在确定权限获取完成之后调用开启定位方法
// [_locationManager startUpdatingLocation];注意 requestWhenInUseAuthorization 和 requestAlwaysAuthorization 的区别:
requestWhenInUseAuthorization只会向用户申请使用应用期间权限,申请权限的弹窗只会有允许和不允许的选项,用户点击允许只能获得使用应用期间权限,若 info.plist 里面正确填写了 always 相关描述,用户还可以手动到隐私设置中选择始终权限。requestAlwaysAuthorization这个方法在 iOS 10 以及之前版本只会弹窗申请始终权限,只有允许和不允许两个选项;从 iOS 11 开始,调用这个方法会出现 3 个选项:『不允许』、『使用应用期间』、『始终允许』,也就是说,即使你调用了requestAlwaysAuthorization方法申请始终权限,用户依然可以选择使用应用期间。
注意:没有按需要在 info.plist 里面写描述字段的,调用相应方法申请权限会导致 crash
至此已经完成了 CLLocationManager 的初始化和权限申请,要使用后台定位功能,接下来还需要设置下面两个属性:
_locationManager.pausesLocationUpdatesAutomatically = NO;
if (@available(iOS 9.0, *)) {
_locationManager.allowsBackgroundLocationUpdates = YES;
}下面解释一下这两个属性。
pausesLocationUpdatesAutomatically#
首先解释一下 pausesLocationUpdatesAutomatically ,这是 iOS 6.0 之后新增的属性,这个属性默认是 YES,允许在不牺牲位置信息准确性的前提下暂停位置更新以节省电池消耗。那么是如何做到不牺牲准确性的呢?首先系统会在它认为位置不大可能发生变化的时候,关闭 GPS 相关硬件来节省电量,例如,用户在使用导航应用的时候停下来吃饭,系统就认为这个时候位置不会变化了。那么系统又是如何识别出这个场景的呢?这取决于另一个属性: activityType ,这个属性是个枚举值:
typedef NS_ENUM(NSInteger, CLActivityType) {
CLActivityTypeOther = 1, // 默认场景,没有特殊操作,只在一段时间位置没有发生改变的时候,暂停更新位置
CLActivityTypeAutomotiveNavigation, // 汽车导航场景,当位置长时间都没有发生改变的时候,会关闭部分GPS硬件,直到位置发生改变
CLActivityTypeFitness, // 健身使用场景,包括徒步、跑步、自行车等,这个场景下室内定位会被关闭,当位置在一段时间内没有明显变化时,会暂停更新位置,注意是"明显变化",与上一条程度不一样
CLActivityTypeOtherNavigation, // 其他导航场景,包括轮船、火车、飞机等,不包括步行导航,当位置在一段时间内没有明显变化时,会暂停更新位置
CLActivityTypeAirborne API_AVAILABLE(ios(12.0), macos(10.14), tvos(12.0), watchos(5.0)), // 这个是空中使用场景,这个场景很少用到。。。
};当定位被暂停时,系统会调用 locationManagerDidPauseLocationUpdates: 回调,可以自行在里面进行一些操作,比如设置触发器在用户离开某个区域的时候,发送本地通知提醒用户重新打开 APP 以恢复定位。经测试在 CLActivityTypeOther 场景下,APP 处于后台,位置 180S 左右不发生变化,APP 就被挂起了。所以如果需要实现后台持续定位,这个属性需要设置为 NO。
allowsBackgroundLocationUpdates#
allowsBackgroundLocationUpdates 这个属性是 iOS 9.0 新增的属性,默认为 NO ,设置为 YES 需要保证 Target -> Capabilities -> Background Modes 的开关打开,并勾选 Location updates ,否则会导致 crash。
这个属性是使后台持续定位生效的关键,如果不设置这个属性为 YES ,且 APP 中没有其它机制(如手动续命<最多续10分钟>、播放音乐、VOIP 等)保持 APP 后台运行,那 APP 会在位置信息不再更新之后的 180S 时被挂起。而将这个属性设置为 YES 后,APP 将一直接收定位事件,并且不需要续命也能保持一直在后台运行不被挂起,除非被手动杀死或者被系统回收。这里再提一下上面讲权限时讲到的一点,这个属性被设置为YES 时,即使 APP 选择的是 使用应用期间 权限,在进入后台之后只要不被杀死或回收,APP 同样能一直保持后台持续定位且不被挂起,与选择 始终 的区别仅仅是状态栏会出现蓝色提示横条,以及被杀死之后是否能重新唤醒。
被杀死后无感知自动唤醒#
开启后台定位之后 APP 处于后台的时候虽然不会被挂起了,但不可避免的会出现被手动杀死,或者前台应用占用过多内存导致后台应用被系统回收的情况,这个时候为了继续获取位置,需要再次唤醒 APP 继续接收定位事件。所幸苹果还是提供了这样的方法,供开发者做到通过定位唤醒 APP。
首先这个方法需要 始终 权限,然后在开启定位调用 startUpdatingLocation 的同时调用 startMonitoringSignificantLocationChanges 方法。
[_locationManager startUpdatingLocation];
// 开启位置明显变化监控
[_locationManager startMonitoringSignificantLocationChanges];开启了这个方法之后,当位置发生较明显的变化时同样会触发一个位置更新事件,这个变化距离不依据 distanceFilter 属性值变化,而是固定的 500 米(指与上一次触发此事件时的距离),且每 5 分钟最多触发一次,触发的时候如果 APP 被杀死了,这个事件还会唤醒 APP ,APP 会在用户无感知的情况下重新走正常的启动流程,只是界面上没有任何显示,此时会在 application:didFinishLaunchingWithOptions: 方法中携带 UIApplicationLaunchOptionsLocationKey 这个 key 来表明当前启动是被位置事件唤醒的,请注意判断这个 key 来绕过一些不需要在后台启动时执行的逻辑,或者执行一些你想要在后台启动后执行的逻辑,比如启动位置上报服务。在 APP 重新启动之后需要重新调用 startMonitoringSignificantLocationChanges 以保证 APP 再次被杀之后能被重新唤醒。
由此方法触发的位置更新事件同样会调用正常位置更新服务 startUpdatingLocation 的locationManager:didUpdateLocations: 回调方法,这个更新事件可能是一个最近缓存的位置,也可能是最新位置,而更精确的位置会在几秒钟之后再次回调,所以如果对细微位置差别比较敏感的话,可以通过回调的 CLLocation 对象中的时间戳来过滤。
最后,调用 stopMonitoringSignificantLocationChanges 可以停止监控不再唤醒 APP,这个方法的调用也非常重要,当业务逻辑不需要后台持续定位的时候一定要调用停止方法,否则 APP 将无止境地在位置变化超过 500 米之后被重新唤醒,对用户的设备无疑是一个负担,这是一个流氓行为。
省电#
开启了后台持续定位功能之后对于设备的电量毫无疑问会造成巨大的消耗,这个时候可以做一些事情来减少电量的消耗,苹果提供了一个方法:allowDeferredLocationUpdatesUntilTraveled:timeout: ,延迟位置更新,这个方法在 iOS 6.0 新增,可以设定一个移动半径或者时间间隔,当设备处于低功耗模式时,在这个移动半径或者时间间隔内的位置变化将不会触发位置更新事件。
方法有两个参数,第一个参数 (CLLocationDistance)distance 是距离半径,单位是米,也可以设置为 CLLocationDistanceMax 来不限制半径;第二个参数 (NSTimeInterval)timeout 是时间间隔,单位是秒,也可以设置为 CLTimeIntervalMax 来不限制时间间隔。这两个参数任一条件达到都会立即触发一次位置更新事件,同样是调用locationManager:didUpdateLocations: 回调,并且触发完成一次之后还将触发 locationManager:didFinishDeferredUpdatesWithError: 方法告知本次延迟更新结束,需要再次使用延迟更新需要重新调用 allowDeferredLocationUpdatesUntilTraveled:timeout: 方法。苹果的建议是直接在locationManager:didUpdateLocations: 回调中调用此方法,在你处理完最新位置之后即可根据需求发起延迟位置更新,这样当 APP 处于后台的时候延迟更新方法就有机会在这个回调中被执行。
需要注意的是,只有在设备处于低功耗模式的时候延迟更新位置才会生效,低功耗模式是系统控制进入的一个模式(注意不是常说的省电模式),只可能发生在屏幕关闭时由电源管理策略自动进入,并且只有 iPhone 5s 之后具备 M 系列协处理器的设备才能进入低功耗模式,使用延迟更新方法之前可以通过 [CLLocationManager deferredLocationUpdatesAvailable] 方法来判断设备是否支持,另外 连接 Xcode 调试的设备不会进入低功耗模式 。
除此以外,使用延迟更新方法还需要同时满足以下条件:
设备 GPS 模块可用,所以模拟器、 iPad 、 iPod 就不支持了
desiredAccuracy需要设置为kCLLocationAccuracyBest或者kCLLocationAccuracyBestForNavigationdistanceFilter需要设置为kCLDistanceFilterNone
以上条件缺一不可。
