最近在做公司内部的一个项目。主要需求很简单,就是每隔N分钟向服务器发送设备的位置,不管此时App是运行在前台还是后台。
这里总结一下使用iOS定位服务的一些关键点和需要注意的地方。
App 的设置
- 因为App需要在后台的时候也能不断地获取设备的位置。所以要将Capablities里面的BackgroundMode 设置成Enable。并且勾选其中的Location updates选项。
- 在iOS8以后,需要在info.plist里面添加NSLocationAlwaysUsageDescription或者NSLocationWhenInUseUsageDescription,这两个key都是NSString类型。使用哪个(或者两者都添加)取决于申请定位的权限,这个下文会提到。这个所谓的描述就是当系统提示用户App要使用定位的时候,会加在系统提示的后面,如图。
初始化CLLocationManager
使用iOS定位服务需要引入系统的头文件并且实现CLLocationManagerDelegate的代理。
#import <CoreLocation/CoreLocation.h>
先来看一下初始化的代码:
-(void) createLocationManager{
_locationManager = [[CLLocationManager alloc] init];
_locationManager.delegate = self;
if ([_locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) {
[_locationManager requestAlwaysAuthorization];
}
if ([_locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
[_locationManager setAllowsBackgroundLocationUpdates:YES];
}
_locationManager.pausesLocationUpdatesAutomatically = NO;
}
iOS8以后,系统的定位权限有三种,对应设置里面的总是,永不,和App使用期间。那么根据我们App的需求,我们需要申请“总是”这种权限。相应地,我们要在info.plist里面添加的是NSLocationAlwaysUsageDescription。
if ([_locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) {
[_locationManager requestAlwaysAuthorization];
}
并且在iOS9之后,如果需要在后台保持定位,除了上文所说的在App的setting和info文件里面设置以外,还需要加上下面的代码:
if ([_locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
[_locationManager setAllowsBackgroundLocationUpdates:YES];
}
整个初始化完成以后,调用以下API系统就会开始定位了
[_locationManager startUpdatingLocation];
在代理里面实现位置更新的代码
正常来说,完成上面的所有设置,就可以使用iOS系统的定位服务了。
系统会每秒都调用
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
这个代理方法,我们所要做的就是在这里处理系统返回回来的位置信息。
CLLocation这个类里面包括的一些常用的位置信息有经度、纬度、海拔、速度、精确度等等,根据项目的需求可以对其进行相应的处理。
到这里,最基础的部分已经完成。接下来会探讨一些别的配置。
pausesLocationUpdatesAutomatically属性
贴上一段官网对这个属性的描述:
Allowing the location manager to pause updates can improve battery life on the target device without sacrificing location data. When this property is set to YES, the location manager pauses updates (and powers down the appropriate hardware) at times when the location data is unlikely to change. For example, if the user stops for food while using a navigation app, the location manager might pause updates for a period of time. You can help the determination of when to pause location updates by assigning a value to the activityTypeproperty.
大致的意思就是如果这个属性设置成YES(默认的也是YES),那么系统会检测如果设备有一段时间没有移动,就会自动停掉位置更新服务。这里需要注意的是,一旦定位服务停止了,只有当用户再次开启App的时候定位服务才会重新启动。
这里的一段时间是系统自动判定的,可以通过设置activityTypeproperty这个属性来决定这个时间的长短。
API的意思是,类似导航类的App,系统检验的时间会稍长一点,想运动类的App,就会比导航类的短一点。但是具体时间还是由系统来决定。
DeferredUpdates
默认地,定位服务的代理会每秒钟都更新一次位置,这样对电池的消耗量会特别地大。除了设置pausesLocationUpdatesAutomatically这个属性以外,iOS还提供了DeferredUpdates的机制。
官方API文档:
- (void)allowDeferredLocationUpdatesUntilTraveled:(CLLocationDistance)distance
timeout:(NSTimeInterval)timeout
distance:
The distance (in meters) from the current location that must be travelled before event delivery resumes. To specify an unlimited distance, pass the CLLocationDistanceMaxconstant.
timeout:
The amount of time (in seconds) from the current time that must pass before event delivery resumes. To specify an unlimited amount of time, pass the CLTimeIntervalMax constant.
就是你可以设置让系统每隔多远或者每隔多长时间更新一次位置。注意是“或”的关系,满足一个就会更新。
使用这个方法有很多要注意的地方:
- desiredAccuracy必须设置成kCLLocationAccuracyBest
- distanceFilter必须设置成kCLErrorDeferredDistanceFiltered
- 必须能够使用GPS进行定位(而不仅仅是移动数据或者Wi-Fi)
- 非常重要的一点,DeferredUpdates只会出现在设备进入低耗电量的状态,App运行在前台或者设备连接在Xcode上正在调试是不会触发的。(所以不可能在Debug的时候打印Log来检验,要调试的话,需要写一些Log存在本地的数据库)
官网的Example:
-(void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations {
// Add the new locations to the hike
[self.hike addLocations:locations];
// Defer updates until the user hikes a certain distance or a period of time has passed
if (!self.deferringUpdates) {
CLLocationDistance distance = self.hike.goal - self.hike.distance;
NSTimeInterval time = [self.nextUpdate timeIntervalSinceNow];
[self.locationManager allowDeferredLocationUpdatesUntilTraveled:distance timeout:time];
self.deferringUpdates = YES;
} }
-(void)locationManager:(CLLocationManager *)manager
didFinishDeferredUpdatesWithError:(NSError *)error {
// Stop deferring updates
self.deferringUpdates = NO;
// Adjust for the next goal
}
反地理编码
知道了经纬度,有时候我们需要获取这个经纬度对应的详细地址信息,示例如下:
CLGeocoder *revGeo = [[CLGeocoder alloc] init];
[revGeo reverseGeocodeLocation:location
completionHandler:^(NSArray *placemarks, NSError *error) {
if (!error && [placemarks count] > 0)
{
NSDictionary *dict =
[[placemarks objectAtIndex:0] addressDictionary];
NSArray *formattedLines = [dict objectForKey:@"FormattedAddressLines"];
NSString *formattedAddress = formattedLines[0];
NSLog(@"address is %@",formattedAddress);
}else{
NSLog(@"ERROR: %@", error);
}
}];
关于坐标系的问题
最后讲一下关于坐标系的问题。
世界通用的坐标系是WGS坐标系,中国国测局的坐标系是GCJ,百度有自己的坐标系。
同样的经纬度应用在不同的坐标系会有所偏差,在Github上面有一个库可以实现不同坐标系之间的转化:
https://github.com/TinyQ/TQLocationConverter
系统返回的自然是根据WGS定位的。如果使用百度SDK获取的就是Baidu坐标系的。