iOS定位服务设计实例一则

iOS定位服务设计实例一则

当前,越来越多的移动应用基于LBS(位置服务)构建业务,LBS可以说是移动应用浪潮的基石。每次说到LBS,我们的第一反应就是以百度地图SDK为代表的第三方框架(类似的还有高德,腾讯出品的地图SDK),刻意忽略原生框架Core Location和Map Kit。但问题是:前者真的要比后者好吗?我们对二者到底了解多少?


A. 定位服务和地图服务

位置服务由两部分组成:

  1. 定位:即设备位置信息。
  2. 地图:即显示地图和标注地图。

本文重点介绍定位(服务)。


A.1 定位服务提供的信息

通常,定位服务需要提供的信息可划分为两类:

  1. 设备当前坐标,海拔,朝向等基本位置信息

    这些信息由设备上的相关硬件直接提供,不依赖服务器。

  2. 需要检索服务器的地理信息,如坐标反地理编码(城市,地区,街道,门牌号,名称等等),以及基于关键字的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进行定位的优势如下:

  1. 系统级别的定位信息缓存,,即使CLLocationManager对象刚创建,也可以从中读取到最近一次定位信息。这是因为iOS在系统层面管理定位行为。
  2. 无须任何认证,即可使用,从而保证了服务的稳定性。
  3. 有多个节能设置。由于定位非常耗电,为了增加设备续航,节能就变得异常重要。例如:

/// 使用定位信息的活动类型
@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。

至于更新策略,由于每次查询都是一次网络请求,所以有两种:

  1. 使用时再检索,异步实现。适合用量较少的场景;
  2. 定期检索,保存结果,同步实现。适合用量较大的场景;

根据实际情况,我们选用第二种。

  1. 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 区分检索类型

两种检索类型:

  1. 反地理编码:根据坐标查询街道,城市等信息;
  2. poi:根据关键字,查询"兴趣点"(point of interest);

D.3 问题

百度地图SDK的接入,会带来两个问题:

  1. 百度地图SDK要求在app启动时进行注册,否则无法调用服务;
  2. 百度地图服务必须配合SDK自带的定位服务使用;

因此,还需处理以下逻辑:

  1. 注册百度地图SDK;
  2. 封装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 随地图对象释放,不驻留内存

由于这个类仅服务百度地图,所以其应该随着地图的创建而创建,释放而释放。

参考资料:

  1. Location and Maps Programming Guide
  2. 百度地图-iOSSDK-开发指南
  3. 百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系互转

你可能感兴趣的:(iOS定位服务设计实例一则)