iOS定位服务设计实例一则
当前,越来越多的移动应用基于LBS(位置服务)构建业务,LBS可以说是移动应用浪潮的基石。每次说到LBS,我们的第一反应就是以百度地图SDK为代表的第三方框架(类似的还有高德,腾讯出品的地图SDK),刻意忽略原生框架Core Location和Map Kit。但问题是:前者真的要比后者好吗?我们对二者到底了解多少?
A. 定位服务和地图服务
位置服务由两部分组成:
- 定位:即设备位置信息。
- 地图:即显示地图和标注地图。
本文重点介绍定位(服务)。
A.1 定位服务提供的信息
通常,定位服务需要提供的信息可划分为两类:
-
设备当前坐标,海拔,朝向等
基本位置信息
。这些信息由设备上的相关硬件直接提供,不依赖服务器。
-
需要检索服务器的地理信息,如坐标反地理编码(城市,地区,街道,门牌号,名称等等),以及基于关键字的poi查询(兴趣点)等。
这些信息通过向服务器检索获取,可以视为是基于第一类信息的延伸。
B. 技术方案
经考量,使用如下策略获取这两类信息:
-
基本位置信息:通过原生框架
Core Location
获取。原因如下:- 配置项多,有助于精细化控制服务;
- 信息全面,来源统一;
- 提供多个节能选项;
- 相较于第三方框架(如百度地图SDK),无须认证(否则如果百度服务挂了,搞的最基本的定位服务也不能用);
-
地理信息检索:通过
百度地图SDK
获取。原因如下:- 检索种类多,信息全面(特别是poi信息);
- 模块清晰,使用简单;
- 可以配合百度地图服务一起使用;
下面,我们开始编写自己的定位服务
C. 定位服务
假设我们要编写一个名为LocationService
的定位服务,负责提供定位信息。根据需求,我们为其定义如下Interface:
/// 单例,全局唯一入口
+ (instancetype)defaultService;
@property (nonatomic, strong, readonly) DDPCLocation *ddpcLocation;
@property (nonatomic, assign, readonly) CLLocationCoordinate2D coordinate;
具体功能&特性如下:
C.1 申请位置信息访问权限
众所周知,位置信息访问权限有两种:
- When In Use(app在前台时)
- Always(app运行时,不管在前台还是后台)
大家也许会注意到,有些app被切换至后台,状态栏处会出现蓝条,显示一条信息:xxxx正在使用你的位置。
所以,上述权限除了字面所示的区别之外,还有一点需要注意:app被切换至后台,如果开启了后台位置更新,则:
- When In Use:显示蓝条
- Always:不显示蓝条
合理的解释是,对于Always来说,系统认为用户已经充分知晓app会在后台继续访问位置信息,所以不必提示;而对于When In Use来说,系统认为有必要提醒用户app正在后台继续访问位置信息,超出了授权的范围。
请求授权的代码如下:
/*** LocationService init ***/
// 请求授权,always
[self.locationManager requestAlwaysAuthorization];
C.2 使用CLLocationManager获取基本位置信息
使用CLLocationManager进行定位的优势如下:
- 系统级别的定位信息缓存,,即使CLLocationManager对象刚创建,也可以从中读取到最近一次定位信息。这是因为iOS在系统层面管理定位行为。
- 无须任何认证,即可使用,从而保证了服务的稳定性。
- 有多个节能设置。由于定位非常耗电,为了增加设备续航,节能就变得异常重要。例如:
/// 使用定位信息的活动类型
@property(assign, nonatomic) CLActivityType activityType;
/// 当设备位置可能不再变化时,系统是否可以自动暂停位置更新
@property(assign, nonatomic) BOOL pausesLocationUpdatesAutomatically;
/// 移动多少距离,才触发位置更新
@property(assign, nonatomic) CLLocationDistance distanceFilter;
C.3 当前坐标一直有效
当前,用户未授权访问位置信息时除外。
正如C.1.2所述,iOS在系统层面缓存了最新定位信息。事实上,CLLocationManager对象一旦创建,就可以从其属性location
处获取最近一次定位信息,代码如下:
/*** LocationService init ***/
// 读取locationManager中的位置缓存
self.rawLocation = self.locationManager.location;
随后,一旦有位置更新,我们再将其缓存起来使用:
/*** CLLocationManagerDelegate ***/
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
// 每次位置更新,记录新的坐标
self.rawLocation = locations.lastObject;
}
提醒一点,上述方法中,最新位置的时间戳总是和manager.location的时间相同。也就是说,CLLocationManager首先保存最新位置,再调用进行回调。
C.4 减少不必要的位置更新,尽可能节能
正常情况下,一旦开始监听,就会源源不断的收到位置更新,即使设备在原地保持不动。还要注意,更新的频率很高,粗略估算,平均每10秒左右就会有一次更新。很明显,这种信息重复的高频率更新并不是必须的,很多时候"有移动,才更新"的模式更适合业务需求。
例如,后台要求设备每移动10m,就上报一次位置,那么可以按照如下配置CLLocationManager:
/*** _locationManager = [[CLLocationManager alloc] init]; ***/
_locationManager.distanceFilter = 10.0;
_locationManager.desiredAccuracy = kCLLocationAccuracyBest;
这样,既保证了定位的精度,也减少了更新频率,节省设备电力。
C.5 处理位置授权状态变更
下述回调不仅在授权状态变更时触发,也会在CLLocationMananger创建后触发:
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
所以,这个方法可以是许多关键逻辑的入口。例如,下面的代码首先判断授权状态:如果得到授权,则开始监听位置更新;读取定位缓存;进行反地理编码查询。如果未得到授权,则可以尝试提醒用户。
/*** locationManager:didChangeAuthorizationStatus: ***/
if (self.isAuthorized) { // 已授权
// 刷新位置
[self.locationManager startUpdatingLocation];
// 读取locationManager中的位置缓存
self.rawLocation = self.locationManager.location;
// 反地理编码
[self retrieveCurrentLocation];
// 反地理编码timer
[self setupTimer];
} else {
// TODO: 是否要提醒用户打开位置服务
}
C.6 系统坐标转换为百度坐标
一般来说,整个业务体系会使用同一套坐标系,这里假设是百度坐标。原生定位框架给出的坐标是地球坐标,需要客户端进行转换。
```s
不同坐标系:地球坐标,火星坐标和百度坐标
- 地球坐标(WGS84):国际标准,通过Core Location获取的坐标使用这个坐标系;
- 火星坐标(GCJ-02):中国标准,高德地图使用这个坐标系;
- 百度坐标(BD-09):百度地图使用的坐标系;
```
百度地图SDK在计算工具模块给出了现成的转换方式:
// 原始坐标
CLLocationCoordinate2D coor = CLLocationCoordinate2DMake(39.90868, 116.3956);
// 转换WGS84坐标至百度坐标(加密后的坐标)
NSDictionary *testdic = BMKConvertBaiduCoorFrom(coor,BMK_COORDTYPE_GPS);
// 解密加密后的坐标字典
// 转换后的百度坐标
CLLocationCoordinate2D baiduCoor = BMKCoorDictionaryDecode(testdic);
C.7 提供并更新反地理编码信息
反地理编码的具体实现依赖百度地图SDK。
至于更新策略,由于每次查询都是一次网络请求,所以有两种:
- 使用时再检索,异步实现。适合用量较少的场景;
- 定期检索,保存结果,同步实现。适合用量较大的场景;
根据实际情况,我们选用第二种。
- cllocationmanager的distanfiler的问题:
distanceFileter能够只在移动特定距离时,才调用更新方法,配合locationManager的location属性,后者是最新的,但只是距离未达标,才没有调用更新方法,
D. 地理信息检索
负责检索工具类名为GeoSearchOperation
,其是对百度地图SDK检索操作的封装。根据需求,我们为其定义如下Interface:
/// 反地理编码查询
- (void)reverseGeoCodeSearchWithCoordinate:(CLLocationCoordinate2D)coor completionHandler:(void (^)(BOOL isSuccessful, NSArray *results))handler;
/// 检索室内poi,如果city传nil,则表示使用当前定位所在城市,keyword必传
- (void)poiSearchWithCity:(NSString *)city keyword:(NSString *)keyword completionHandler:(void (^)(BOOL isSuccessful, NSArray *results))handler;
具体功能&特性如下:
D.1 使用百度地图SDK检索模块实现
在信息检索方面,由于Core Location框架提供的信息有限,本土化的第三方位置服务框架表现更优秀。
D.2 区分检索类型
两种检索类型:
- 反地理编码:根据坐标查询街道,城市等信息;
- poi:根据关键字,查询"兴趣点"(point of interest);
D.3 问题
百度地图SDK的接入,会带来两个问题:
- 百度地图SDK要求在app启动时进行注册,否则无法调用服务;
- 百度地图服务必须配合SDK自带的定位服务使用;
因此,还需处理以下逻辑:
- 注册百度地图SDK;
- 封装SDK定位服务,供百度地图服务使用;
E. 注册百度地图SDK
BMKAuthentication
负责注册百度地图SDK,并处理可能发生的错误。根据需求,我们为其定义如下Interface:
/// 单例,全局唯一入口
+ (instancetype)defaultAuthentication;
/// 注册百度地图
- (void)authenticate;
具体功能&特性如下:
E.1 尽早注册
在app生命周期的初始阶段,尽可能早的完成注册。由于业务围绕LBS展开,所以app在很多方面强依赖于百度SDK;无法调用SDK服务,意味着业务瘫痪。
一般来说,在app启动之初注册即可,但最好先于其他逻辑:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[DDBMKAuthentication defaultAuthentication] authenticate];
// 其他启动逻辑
}
E.2 失败重试
只在由于网络原因失败时重试,其他情况一律不重试,因为没有意义。例如,因为ak失效或配额超限而注册失败,不管怎么重试,都毫无意义。
BMKMapManager通过下述回调方法告诉我们网络是否存在问题:
- (void)onGetNetworkState:(int)iError {
if (iError == 0) {
self.hasNetworkFailure = NO;
} else {
NSLog(@"网络错误,百度地图注册失败");
self.hasNetworkFailure = YES;
}
}
F. 封装SDK定位服务
DDBMKLocationService
是对百度地图SDK定位服务的封装。根据需求,我们为其定义如下Interface:
/// 进行定位,定位成功,代理方法被调用。注意,只定位一次。
- (void)locate;
/// delegate
@property (nonatomic, weak) id delegate;
/** 百度 location */
@property (nonatomic, strong, readonly) BMKUserLocation *BMKUserLocation;
此外,其还定义了协议DDBMKLocationServiceDelegate
,作为定位成功后的回调。
@protocol DDBMKLocationServiceDelegate
/// 百度sdk定位更新,会调用这个方法
- (void)BMKLocationService:(DDBMKLocationService *)BMKLocationService didUpdateBMKUserLocation:(BMKUserLocation *)userLocation;
@end
具体功能&特性如下:
F.1 仅定位一次,不持续定位
每次调用定位方法locate
,仅定位一次,一旦回调,不管成功或失败,都停止,不持续定位。从而避免了在功能上与LocationService
重叠,也节省了资源。
// 定位
- (void)locate {
[self.BMKLocationService startUserLocationService];
}
#pragma mark - BMKLocationServiceDelegate
- (void)didUpdateBMKUserLocation:(BMKUserLocation *)userLocation {
NSLog(@"百度定位成功✨");
self.BMKUserLocation = userLocation;
// 停止定位
[self.BMKLocationService stopUserLocationService];
}
F.2 随地图对象释放,不驻留内存
由于这个类仅服务百度地图,所以其应该随着地图的创建而创建,释放而释放。
参考资料:
- Location and Maps Programming Guide
- 百度地图-iOSSDK-开发指南
- 百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系互转