Android地理位置服務解析
GPS
基于衛星發射的信號,可以推算出手機到每顆衛星的距離,根據衛星的位置,推測出手機的位置。
這是一張簡單的GPS定位原理圖,需要一點數學知識,先不討論這個細節,需要的同學看 這里 。
現在衛星信號全球都覆蓋了,手機一般都有GPS芯片,因此可以實現定位。GPS方式準確度是最高的,走衛星通道,不需要聯網就可以要使用。但是它的缺點也非常明顯:
- 1.比較耗電;
- 2.絕大部分用戶默認不開啟GPS模塊,也不會長時間開著;
- 3.從GPS模塊啟動到獲取第一次定位數據,可能需要 比較長的時間 ;
- 4. 只能在戶外使用 ,當有遮擋物干擾時,幾乎無法使用,如城市大樓密集的地方。
WiFi
通過獲取當前所連接的WiFi熱點的一些信息,然后訪問定位服務以獲得經緯度坐標。
這是一張簡單的WiFi定位原理圖。
因為WiFi熱點一般都是固定位置,所以只要能知道手機連接的WiFi熱點的位置,也就可以推算出手機的位置。而且由于手機一般連接的WiFi不會太遠,所以其實精確度也不會太差。也不會像GPS那樣需要耗時比較久才能獲得位置信息。
Cell-ID
采集到手機所連接的基站ID號(cellid)和其它的一些信息(MNC,MCC,LAC等),然后通過網絡訪問定位服務,獲取并返回對應的經緯度坐標。
這是一張簡單的基站定位原理圖。
現在各大運營商的基站已經覆蓋了全國大部分地區,每個基站的ID號是全球唯一的,只要有手機信號,就能接收到周圍基站的信號。基站定位的精確度不如GPS,但優點是能夠在室內用,只要網絡通暢就行。
其實各種定位方式,大體都是基于三角定位的原理,不過計算的時候會有一些自己的特點,這里先不深究背景知識了。下面進入正題。
Android系統上如何獲取地理位置
方法1:Google Play Service提供的API
這個不多說,因為國內不可用!!!
需要的同學可以自己爬梯子看下用法,比較簡單: https://developer.android.com/google/play-services/location.html
方法2:系統提供的原生API:主要就是系統的 android.location 中提供的兩個類。
- LocationManager: 和大多數系統提供的 SystemService 一樣是單例,通過 locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); 來獲取。
- LocationListener: 非常典型的觀察者模式,需要監聽地理位置的時候,創建一個 Listener ,實現LocationListener中的幾個回調方法。把Listener傳給LocationManager,當地理位置變化的時候就會回調 onLocationChanged(Location location) 發出通知。
- 官方指導: https://developer.android.com/guide/topics/location/strategies.html
方法3:使用百度、高德之類的地圖SDK。
這簡直就是大招了,各家都有自己的數據庫,比起系統提供的API強太多了。這個這次也不說,各家的接入文檔寫的很清楚。
使用原生API采集地理位置的方法
下面介紹一下我對使用原生API的理解,畢竟不是所有場景都需要用到大招級別的sdk,有的情況我們需要自己實現定位服務。
1.首先需要了解PROVIDER
看過前面介紹的3種定位方式之后,可以很容易理解PROVIDER是什么。其實它就對應著地理位置采集的幾種方式:
- LocationManager.GPS_PROVIDER:通過gps來獲取地理位置的經緯度信息,優點:獲取地理位置信息精確度高,缺點:只能在戶外使用, 耗時,耗電 。
- LocationManager.NETWORK_PROVIDER:通過移動網絡的基站或者WiFi來獲得地理位置,優點:只要有網絡,獲取速度快,耗電低,在室內室外都可以使用。
- LocationManager.PASSIVE_PROVIDER:被動的接收更新的地理位置信息,而不用自己主動請求地理位置。意思就是共享手機上其他App采集的位置信息,而不是自己主動去采集。
下圖是3種Provider的特點和區別:
2.打開手機的設置
先看下原生系統中地理位置設置的界面截圖:
以原生系統為例,需要采集地理位置時,需要:
- 打開通知欄的GPS開關
- 進入 設置->位置信息->模式 ,打開開關。然后我們可以看到,這里有3類模式:
- 高精確度:使用GPS、WLAN、藍牙或者移動網絡確定位置
- 節電:使用WLAN、藍牙或者移動網絡確定位置
- 僅限設備:使用GPS確定位置
PS:我發現小米手機上,即使你把通知欄里面地理位置開關關閉了,進入系統的設置界面,還是可以看到地理位置是開啟的,默認選擇的是 節電 模式。而原生系統你只要在通知欄關閉了開關,就無法使用定位服務了。這里感覺國內廠商在細節上可能會有一些不同的實現。
3.給你的App注冊權限
當你在代碼里面使用3種不同的Provider時,應該關注到兩個權限:
- LocationManager.GPS_PROVIDER:android.permission.ACCESS_FINE_LOCATION
- LocationManager.NETWORK_PROVIDER:android.permission.ACCESS_COARSE_LOCATION 或者 android.permission.ACCESS_FINE_LOCATION。
- 當聲明ACCESS_FINE_LOCATION時,拿到的位置信息將更精確(幾十米到幾百米)
- 當聲明ACCESS_COARSE_LOCATION時,拿到的位置會粗略一點(幾百米到幾千米)
- LocationManager.PASSIVE_PROVIDER:android.permission.ACCESS_COARSE_LOCATION </ul>
注意:如果聲明了ACCESS_FINE_LOCATION時,就不用再聲明ACCESS_COARSE_LOCATION了,因為ACCESS_FINE_LOCATION已經包含了使用NETWORK_PROVIDER的能力。此外從Android6.0開始,ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION已經是 dangerous permission ,開發者需要注意這一點,當用戶在運行你的App時,如果沒有授權,仍然是無法獲取到地理位置信息的。
4.根據需求的場景寫代碼(記住要盡量省電)
一定要省電:這是一個非常重要的用戶體驗,我們應該對自己做的App負責。什么時候開始使用地理位置服務,什么時候停止使用,我們一定要想清楚,盡量不要一直占用著這種高耗電的資源。
4.1基本代碼
下面看代碼,一段基本的獲取地理位置的代碼是這么寫的,這段代碼可以讓你通過異步的方式獲取到用戶的地理位置。
// 獲得Location Manager的實例 LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
// 定義一個監聽器,實現onLocationChanged方法,在這個方法里面可以拿到更新后的地理位置 LocationListener locationListener = new LocationListener() { public void onLocationChanged(Location location) { // 新的Location值在這里返回,Location實例中包含著緯度、經度、海拔、精確度、更新時間等一系列信息。 makeUseOfNewLocation(location); }
public void onStatusChanged(String provider, int status, Bundle extras) {} public void onProviderEnabled(String provider) {} public void onProviderDisabled(String provider) {}
};
// 注冊監聽器,當地理位置變化時,發出通知給Listener。這個方法很關鍵。4個參數需要了解清楚: // 第1個參數:你所使用的provider名稱,是個String // 第2個參數minTime:地理位置更新時發出通知的最小時間間隔 // 第3個參數minDistance:地理位置更新發出通知的最小距離,第2和第3個參數的作用關系是“或”的關系,也就是滿足任意一個條件都會發出通知。這里第2、3個參數都是0,意味著任何時間,只要位置有變化就會發出通知。 // 第4個參數:你的監聽器 locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListener);</code></pre>
4.2如何優化
但是實戰中一定要盡量去優化,雖然獲取地理位置只能是異步的,但是仍然不建議一直不停地監聽地理位置的變化。
谷歌官方也給出了一個采集地理位置的思路,非常值得我們來參考。思路的基本步驟如下:
- 啟動應用。
- 當用戶進入到應用中需要使用地理位置場景時,選擇一個合適的Provider,開始監聽地理位置的變化。
- 獲取系統中緩存的上次的地理位置 LastKnownLocation ,保存到當前地理位置變量 currentLocation 中作為備選值,當拿到新的地理位置后,對比兩者,選擇最優的那個繼續保存它。
- 停止監聽地理位置的變化。
- 使用當前維護著的這個Location作為用戶的位置。
谷歌還給出了這個方案的一個timeline圖示。
4.3關鍵問題
我們比較關注下面4點:
- 1.如何選擇一個最好的provider?
- 2.什么時候開始監聽地理位置變化,什么時候結束?
- 3.如何比較兩個地理位置,決定哪個更好?
- 4.LastknownPostion怎么獲取,怎么使用?
下面介紹我的想法:
第1點:如何選擇一個最好的provider?
這需要看你的需求。系統中也提供了一些方法來幫我們選擇,可以設定一個條件 Criteria ,指定帥選最符合條件的地理位置提供者,根據Cirteria指定的條件,設備會自動選擇哪種location provider。
代碼如下:
Criteria criteria = new Criteria();// criteria.setAccuracy(Criteria.ACCURACY_FINE);//設置定位精準度 criteria.setAltitudeRequired(false);//是否要求海拔 criteria.setBearingRequired(true);//是否要求方向 criteria.setCostAllowed(true);//是否要求收費 criteria.setSpeedRequired(true);//是否要求速度 criteria.setPowerRequirement(Criteria.POWER_LOW);//設置相對省電 criteria.setBearingAccuracy(Criteria.ACCURACY_HIGH);//設置方向精確度 criteria.setSpeedAccuracy(Criteria.ACCURACY_HIGH);//設置速度精確度 criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);//設置水平方向精確度 criteria.setVerticalAccuracy(Criteria.ACCURACY_HIGH);//設置垂直方向精確度
// 返回滿足條件的,當前設備可用的location provider // 當第2個參數為false時,返回當前設備所有provider中最符合條件的那個(但是不一定可用)。 // 當第2個參數為true時,返回當前設備所有可用的provider中最符合條件的那個。 String rovider = mLocationManager.getBestProvider(criteria,true);</code></pre>
總之,一共就3個provider,其實對于大部分開發者,選來選去就是 gps or network 。
第2點,什么時候開始,什么時候結束?
我認為最好開啟了監聽器后,要盡可能早地結束它。也就是不要調用了 requestLocationUpdates(provider, minTime, minDistance, listener) 讓位置服務開始工作后,很長時間都不去 removeUpdates(listener) 來停止服務。
雖然在 requestLocationUpdates 方法中,有 minTime 、 minDistance 參數可以設置。比如設置了60000ms的minTime,希望采更新完一次地理位置后休息60s。或者設置2000米的minDistance,希望位置變化不超過2公里,也休息。這樣做 看起來好像 是可以省電。
但是實測中發現,如果調用 requestLocationUpdates(LocationManager.GPS_PROVIDER, 60000, 2000, listener) 注冊監聽器后,系統的狀態欄上面的GPS那個小圖標一直在顯示。只要你不 removeUpdates(listener) ,他就一直在工作。其實我理解,即使你設置了minTime和minDistance,位置服務還是一直處于工作狀態的,不然它怎么知道位置變化超過了你設定的minDistance呢?
所以我的建議是,當你調用 requestLocationUpdates 后,還應該是設置一個定時器,比如30s。當30s時間到了之后,就 removeUpdate ,不再監聽地理位置了,轉而使用備選的LastKnownLocation。當下次需要使用地理位置時,再重新注冊監聽器,監聽30s,然后就移除監聽器。如果對實時性要求高,我們可以在用戶進入App中某個需要定位服務的場景之前,采用這個方法獲取一次地理位置,把它保存下來。
第3點,如何比較兩個Location,選出更好的那個?
谷歌也給出了代碼示例,先看一下。
private static final int TWO_MINUTES = 1000 60 2;
/** Determines whether one Location reading is better than the current Location fix
- @param location The new Location that you want to evaluate
@param currentBestLocation The current Location fix, to which you want to compare the new one */ protected boolean isBetterLocation(Location location, Location currentBestLocation) { if (currentBestLocation == null) {
// A new location is always better than no location return true;
}
// Check whether the new location fix is newer or older long timeDelta = location.getTime() - currentBestLocation.getTime(); boolean isSignificantlyNewer = timeDelta > TWO_MINUTES; boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES; boolean isNewer = timeDelta > 0;
// If it's been more than two minutes since the current location, use the new location // because the user has likely moved if (isSignificantlyNewer) {
return true;
// If the new location is more than two minutes older, it must be worse } else if (isSignificantlyOlder) {
return false;
}
// Check whether the new location fix is more or less accurate int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); boolean isLessAccurate = accuracyDelta > 0; boolean isMoreAccurate = accuracyDelta < 0; boolean isSignificantlyLessAccurate = accuracyDelta > 200;
// Check if the old and new location are from the same provider boolean isFromSameProvider = isSameProvider(location.getProvider(),
currentBestLocation.getProvider());
// Determine location quality using a combination of timeliness and accuracy if (isMoreAccurate) {
return true;
} else if (isNewer && !isLessAccurate) {
return true;
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
return true;
} return false; }
/* Checks whether two providers are the same / private boolean isSameProvider(String provider1, String provider2) { if (provider1 == null) { return provider2 == null; } return provider1.equals(provider2); }</code></pre>
這段代碼的策略是:
- 1.先看更新時間:設定一個時間范圍,2分鐘。
- 如果新的Location比舊的Location獲取時間更新,且超過2分鐘,那么認為新的Location更好。
- 如果新的Location比舊的Location獲取時間更老,且超過2分鐘,那么認為新的Location不夠好。
- 如果新的Location比舊的Location獲取時間更新,但沒有超過2分鐘,那么看下它們的精確度。
- 2.再看精確度:設定一個精確度范圍,200米。
- 如果新的Location比舊的Location精確度更高,那么認為新的Location更好。
- 如果新的Location和舊的Location精確度相等,且獲取時間更新,那么認為新的Location更好。
- 如果新的Location比舊的Location精確度低200m以內,且獲取時間更新,來自同一個provider,那么為認為新的Location更好。
- 其他情況都認為舊的Location更好。 </ul> </li> </ul>
這段代碼是一個參考,我們實際開發中可以更具需要去定義自己的 Better Location 策略。
另外,從API>=17開始,Location類還增加了一個 getElapsedRealtimeNanos 方法(獲取從系統啟動后走過的時間),這是為了解決 getTime 方法(獲取UTC時間)不夠精確,容易產生誤差的問題。這個方法在比較兩個Location時將更加可靠。
第4點,怎么獲取LastknownPostion,怎么使用?
相信有了第3點,應該知道怎么選擇 Better Location 。至于獲取LastKnownLocation直接看代碼。
Location gpsLocation = null; Location networkLocation = null;
if (context.checkCallingOrSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); }
if (context.checkCallingOrSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) { networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); }
// 下面可以比較一下哪個更好... Location currentLocation = gpsLocation; if (isBetterLocation(currentLocation, networkLocation)){ currentLocation = networkLocation; }</code></pre>
總結一下
說了一大堆,我覺得平時開發的時候應該這么做:
- 1.確定自己的應用什么時候要開始監聽地理位置變化,什么時候停止。
- 2.選擇一個合適的provider,開始監聽它提供的地理位置變化。
- 3.讀取系統中GPS和NETWORK這兩個Provide緩存的 LastKnownPostion ,選出Better Location保存到currentBestLocation變量中。
- 4.監聽到地理位置更新后,把更新到的Location和保存的currentBestLocation比較,得出Better One,再保存到currentBestLocation變量中。
- 5.使用currentBestLocation作為用戶的位置,并在合適時機移除監聽器。
來自:http://unclechen.github.io/2016/09/02/Android地理位置服務解析/