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<CLLocation *> *)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<DDBMKLocationServiceDelegate> delegate;
/** 百度 location */
@property (nonatomic, strong, readonly) BMKUserLocation *BMKUserLocation;

此外,其还定义了协议DDBMKLocationServiceDelegate,作为定位成功后的回调。

@protocol DDBMKLocationServiceDelegate <NSObject>

/// 百度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坐标系互转
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容