iOS定位服務(wù)設(shè)計(jì)實(shí)例一則
當(dāng)前,越來越多的移動應(yīng)用基于LBS(位置服務(wù))構(gòu)建業(yè)務(wù),LBS可以說是移動應(yīng)用浪潮的基石。每次說到LBS,我們的第一反應(yīng)就是以百度地圖SDK為代表的第三方框架(類似的還有高德,騰訊出品的地圖SDK),刻意忽略原生框架Core Location和Map Kit。但問題是:前者真的要比后者好嗎?我們對二者到底了解多少?
A. 定位服務(wù)和地圖服務(wù)
位置服務(wù)由兩部分組成:
- 定位:即設(shè)備位置信息。
- 地圖:即顯示地圖和標(biāo)注地圖。
本文重點(diǎn)介紹定位(服務(wù))。
A.1 定位服務(wù)提供的信息
通常,定位服務(wù)需要提供的信息可劃分為兩類:
-
設(shè)備當(dāng)前坐標(biāo),海拔,朝向等
基本位置信息。這些信息由設(shè)備上的相關(guān)硬件直接提供,不依賴服務(wù)器。
-
需要檢索服務(wù)器的地理信息,如坐標(biāo)反地理編碼(城市,地區(qū),街道,門牌號,名稱等等),以及基于關(guān)鍵字的poi查詢(興趣點(diǎn))等。
這些信息通過向服務(wù)器檢索獲取,可以視為是基于第一類信息的延伸。
B. 技術(shù)方案
經(jīng)考量,使用如下策略獲取這兩類信息:
-
基本位置信息:通過原生框架
Core Location獲取。原因如下:- 配置項(xiàng)多,有助于精細(xì)化控制服務(wù);
- 信息全面,來源統(tǒng)一;
- 提供多個節(jié)能選項(xiàng);
- 相較于第三方框架(如百度地圖SDK),無須認(rèn)證(否則如果百度服務(wù)掛了,搞的最基本的定位服務(wù)也不能用);
-
地理信息檢索:通過
百度地圖SDK獲取。原因如下:- 檢索種類多,信息全面(特別是poi信息);
- 模塊清晰,使用簡單;
- 可以配合百度地圖服務(wù)一起使用;
下面,我們開始編寫自己的定位服務(wù)??
C. 定位服務(wù)
假設(shè)我們要編寫一個名為LocationService的定位服務(wù),負(fù)責(zé)提供定位信息。根據(jù)需求,我們?yōu)槠涠x如下Interface:
/// 單例,全局唯一入口
+ (instancetype)defaultService;
@property (nonatomic, strong, readonly) DDPCLocation *ddpcLocation;
@property (nonatomic, assign, readonly) CLLocationCoordinate2D coordinate;
具體功能&特性如下:
C.1 申請位置信息訪問權(quán)限
眾所周知,位置信息訪問權(quán)限有兩種:
- When In Use(app在前臺時)
- Always(app運(yùn)行時,不管在前臺還是后臺)
大家也許會注意到,有些app被切換至后臺,狀態(tài)欄處會出現(xiàn)藍(lán)條,顯示一條信息:xxxx正在使用你的位置。
所以,上述權(quán)限除了字面所示的區(qū)別之外,還有一點(diǎn)需要注意:app被切換至后臺,如果開啟了后臺位置更新,則:
- When In Use:顯示藍(lán)條
- Always:不顯示藍(lán)條
合理的解釋是,對于Always來說,系統(tǒng)認(rèn)為用戶已經(jīng)充分知曉app會在后臺繼續(xù)訪問位置信息,所以不必提示;而對于When In Use來說,系統(tǒng)認(rèn)為有必要提醒用戶app正在后臺繼續(xù)訪問位置信息,超出了授權(quán)的范圍。
請求授權(quán)的代碼如下:
/*** LocationService init ***/
// 請求授權(quán),always
[self.locationManager requestAlwaysAuthorization];
C.2 使用CLLocationManager獲取基本位置信息
使用CLLocationManager進(jìn)行定位的優(yōu)勢如下:
- 系統(tǒng)級別的定位信息緩存,,即使CLLocationManager對象剛創(chuàng)建,也可以從中讀取到最近一次定位信息。這是因?yàn)閕OS在系統(tǒng)層面管理定位行為。
- 無須任何認(rèn)證,即可使用,從而保證了服務(wù)的穩(wěn)定性。
- 有多個節(jié)能設(shè)置。由于定位非常耗電,為了增加設(shè)備續(xù)航,節(jié)能就變得異常重要。例如:
/// 使用定位信息的活動類型
@property(assign, nonatomic) CLActivityType activityType;
/// 當(dāng)設(shè)備位置可能不再變化時,系統(tǒng)是否可以自動暫停位置更新
@property(assign, nonatomic) BOOL pausesLocationUpdatesAutomatically;
/// 移動多少距離,才觸發(fā)位置更新
@property(assign, nonatomic) CLLocationDistance distanceFilter;
C.3 當(dāng)前坐標(biāo)一直有效
當(dāng)前,用戶未授權(quán)訪問位置信息時除外。
正如C.1.2所述,iOS在系統(tǒng)層面緩存了最新定位信息。事實(shí)上,CLLocationManager對象一旦創(chuàng)建,就可以從其屬性location處獲取最近一次定位信息,代碼如下:
/*** LocationService init ***/
// 讀取locationManager中的位置緩存
self.rawLocation = self.locationManager.location;
隨后,一旦有位置更新,我們再將其緩存起來使用:
/*** CLLocationManagerDelegate ***/
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
// 每次位置更新,記錄新的坐標(biāo)
self.rawLocation = locations.lastObject;
}
提醒一點(diǎn),上述方法中,最新位置的時間戳總是和manager.location的時間相同。也就是說,CLLocationManager首先保存最新位置,再調(diào)用進(jìn)行回調(diào)。
C.4 減少不必要的位置更新,盡可能節(jié)能
正常情況下,一旦開始監(jiān)聽,就會源源不斷的收到位置更新,即使設(shè)備在原地保持不動。還要注意,更新的頻率很高,粗略估算,平均每10秒左右就會有一次更新。很明顯,這種信息重復(fù)的高頻率更新并不是必須的,很多時候"有移動,才更新"的模式更適合業(yè)務(wù)需求。
例如,后臺要求設(shè)備每移動10m,就上報(bào)一次位置,那么可以按照如下配置CLLocationManager:
/*** _locationManager = [[CLLocationManager alloc] init]; ***/
_locationManager.distanceFilter = 10.0;
_locationManager.desiredAccuracy = kCLLocationAccuracyBest;
這樣,既保證了定位的精度,也減少了更新頻率,節(jié)省設(shè)備電力。
C.5 處理位置授權(quán)狀態(tài)變更
下述回調(diào)不僅在授權(quán)狀態(tài)變更時觸發(fā),也會在CLLocationMananger創(chuàng)建后觸發(fā):
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
所以,這個方法可以是許多關(guān)鍵邏輯的入口。例如,下面的代碼首先判斷授權(quán)狀態(tài):如果得到授權(quán),則開始監(jiān)聽位置更新;讀取定位緩存;進(jìn)行反地理編碼查詢。如果未得到授權(quán),則可以嘗試提醒用戶。
/*** locationManager:didChangeAuthorizationStatus: ***/
if (self.isAuthorized) { // 已授權(quán)
// 刷新位置
[self.locationManager startUpdatingLocation];
// 讀取locationManager中的位置緩存
self.rawLocation = self.locationManager.location;
// 反地理編碼
[self retrieveCurrentLocation];
// 反地理編碼timer
[self setupTimer];
} else {
// TODO: 是否要提醒用戶打開位置服務(wù)
}
C.6 系統(tǒng)坐標(biāo)轉(zhuǎn)換為百度坐標(biāo)
一般來說,整個業(yè)務(wù)體系會使用同一套坐標(biāo)系,這里假設(shè)是百度坐標(biāo)。原生定位框架給出的坐標(biāo)是地球坐標(biāo),需要客戶端進(jìn)行轉(zhuǎn)換。
```s
不同坐標(biāo)系:地球坐標(biāo),火星坐標(biāo)和百度坐標(biāo)
- 地球坐標(biāo)(WGS84):國際標(biāo)準(zhǔn),通過Core Location獲取的坐標(biāo)使用這個坐標(biāo)系;
- 火星坐標(biāo)(GCJ-02):中國標(biāo)準(zhǔn),高德地圖使用這個坐標(biāo)系;
- 百度坐標(biāo)(BD-09):百度地圖使用的坐標(biāo)系;
```
百度地圖SDK在計(jì)算工具模塊給出了現(xiàn)成的轉(zhuǎn)換方式:
// 原始坐標(biāo)
CLLocationCoordinate2D coor = CLLocationCoordinate2DMake(39.90868, 116.3956);
// 轉(zhuǎn)換WGS84坐標(biāo)至百度坐標(biāo)(加密后的坐標(biāo))
NSDictionary *testdic = BMKConvertBaiduCoorFrom(coor,BMK_COORDTYPE_GPS);
// 解密加密后的坐標(biāo)字典
// 轉(zhuǎn)換后的百度坐標(biāo)
CLLocationCoordinate2D baiduCoor = BMKCoorDictionaryDecode(testdic);
C.7 提供并更新反地理編碼信息
反地理編碼的具體實(shí)現(xiàn)依賴百度地圖SDK。
至于更新策略,由于每次查詢都是一次網(wǎng)絡(luò)請求,所以有兩種:
- 使用時再檢索,異步實(shí)現(xiàn)。適合用量較少的場景;
- 定期檢索,保存結(jié)果,同步實(shí)現(xiàn)。適合用量較大的場景;
根據(jù)實(shí)際情況,我們選用第二種。
- cllocationmanager的distanfiler的問題:
distanceFileter能夠只在移動特定距離時,才調(diào)用更新方法,配合locationManager的location屬性,后者是最新的,但只是距離未達(dá)標(biāo),才沒有調(diào)用更新方法,
D. 地理信息檢索
負(fù)責(zé)檢索工具類名為GeoSearchOperation,其是對百度地圖SDK檢索操作的封裝。根據(jù)需求,我們?yōu)槠涠x如下Interface:
/// 反地理編碼查詢
- (void)reverseGeoCodeSearchWithCoordinate:(CLLocationCoordinate2D)coor completionHandler:(void (^)(BOOL isSuccessful, NSArray *results))handler;
/// 檢索室內(nèi)poi,如果city傳nil,則表示使用當(dāng)前定位所在城市,keyword必傳
- (void)poiSearchWithCity:(NSString *)city keyword:(NSString *)keyword completionHandler:(void (^)(BOOL isSuccessful, NSArray *results))handler;
具體功能&特性如下:
D.1 使用百度地圖SDK檢索模塊實(shí)現(xiàn)
在信息檢索方面,由于Core Location框架提供的信息有限,本土化的第三方位置服務(wù)框架表現(xiàn)更優(yōu)秀。
D.2 區(qū)分檢索類型
兩種檢索類型:
- 反地理編碼:根據(jù)坐標(biāo)查詢街道,城市等信息;
- poi:根據(jù)關(guān)鍵字,查詢"興趣點(diǎn)"(point of interest);
D.3 問題
百度地圖SDK的接入,會帶來兩個問題:
- 百度地圖SDK要求在app啟動時進(jìn)行注冊,否則無法調(diào)用服務(wù);
- 百度地圖服務(wù)必須配合SDK自帶的定位服務(wù)使用;
因此,還需處理以下邏輯:
- 注冊百度地圖SDK;
- 封裝SDK定位服務(wù),供百度地圖服務(wù)使用;
E. 注冊百度地圖SDK
BMKAuthentication負(fù)責(zé)注冊百度地圖SDK,并處理可能發(fā)生的錯誤。根據(jù)需求,我們?yōu)槠涠x如下Interface:
/// 單例,全局唯一入口
+ (instancetype)defaultAuthentication;
/// 注冊百度地圖
- (void)authenticate;
具體功能&特性如下:
E.1 盡早注冊
在app生命周期的初始階段,盡可能早的完成注冊。由于業(yè)務(wù)圍繞LBS展開,所以app在很多方面強(qiáng)依賴于百度SDK;無法調(diào)用SDK服務(wù),意味著業(yè)務(wù)癱瘓。
一般來說,在app啟動之初注冊即可,但最好先于其他邏輯:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[DDBMKAuthentication defaultAuthentication] authenticate];
// 其他啟動邏輯
}
E.2 失敗重試
只在由于網(wǎng)絡(luò)原因失敗時重試,其他情況一律不重試,因?yàn)闆]有意義。例如,因?yàn)閍k失效或配額超限而注冊失敗,不管怎么重試,都毫無意義。
BMKMapManager通過下述回調(diào)方法告訴我們網(wǎng)絡(luò)是否存在問題:
- (void)onGetNetworkState:(int)iError {
if (iError == 0) {
self.hasNetworkFailure = NO;
} else {
NSLog(@"網(wǎng)絡(luò)錯誤,百度地圖注冊失敗");
self.hasNetworkFailure = YES;
}
}
F. 封裝SDK定位服務(wù)
DDBMKLocationService是對百度地圖SDK定位服務(wù)的封裝。根據(jù)需求,我們?yōu)槠涠x如下Interface:
/// 進(jìn)行定位,定位成功,代理方法被調(diào)用。注意,只定位一次。
- (void)locate;
/// delegate
@property (nonatomic, weak) id<DDBMKLocationServiceDelegate> delegate;
/** 百度 location */
@property (nonatomic, strong, readonly) BMKUserLocation *BMKUserLocation;
此外,其還定義了協(xié)議DDBMKLocationServiceDelegate,作為定位成功后的回調(diào)。
@protocol DDBMKLocationServiceDelegate <NSObject>
/// 百度sdk定位更新,會調(diào)用這個方法
- (void)BMKLocationService:(DDBMKLocationService *)BMKLocationService didUpdateBMKUserLocation:(BMKUserLocation *)userLocation;
@end
具體功能&特性如下:
F.1 僅定位一次,不持續(xù)定位
每次調(diào)用定位方法locate,僅定位一次,一旦回調(diào),不管成功或失敗,都停止,不持續(xù)定位。從而避免了在功能上與LocationService重疊,也節(jié)省了資源。
// 定位
- (void)locate {
[self.BMKLocationService startUserLocationService];
}
#pragma mark - BMKLocationServiceDelegate
- (void)didUpdateBMKUserLocation:(BMKUserLocation *)userLocation {
NSLog(@"百度定位成功?");
self.BMKUserLocation = userLocation;
// 停止定位
[self.BMKLocationService stopUserLocationService];
}
F.2 隨地圖對象釋放,不駐留內(nèi)存
由于這個類僅服務(wù)百度地圖,所以其應(yīng)該隨著地圖的創(chuàng)建而創(chuàng)建,釋放而釋放。