做移动互联网就不太可能不碰手机端的开发。上周为了项目需要,俺也挽袖子抡胳膊开始写起了Android程序,还好有java基础,倒也上手快,写了几个小程序,主要都是关于定位方面的。
网上也搜得到一些相关的文章和教程,但给出的例子效果不太好,而且感觉只有其表,却不明其理。因此写出此文,分享一些我的经验。虽然是以Android为主,但是我想对其它平台的开发也应该有些帮助。这篇文章侧重于制定一个合理的定位方案。
手机定位的方式
先科普一些基础知识吧。
最简单的手机定 位方式当然是通过GPS模块(现在大部分的智能机应该都有了)。GPS方式准确度是最高的,但是它的缺点也非常明显:1,比较耗电;2,绝大部分用户默认 不开启GPS模块;3,从GPS模块启动到获取第一次定位数据,可能需要比较长的时间;4,室内几乎无法使用。这其中,缺点2,3都是比较致命的。需要指 出的是,GPS走的是卫星通信的通道,在没有网络连接的情况下也能用。
另外一种常见的定位方式是基站定位。大致思路就是采集到手机上的基站ID号(cellid)和其它的一些信息(MNC,MCC,LAC等等),然后通过网络访问一些定位服务,获取并返回对应的经纬度坐标。基站定位的精确度不如GPS,但好处是能够在室内用,只要网络通畅就行。
还有Wifi定位。和基站定位类似,这种方式是通过获取当前所用的wifi的一些信息,然后访问网络上的定位服务以获得经纬度坐标。因为它和基站定位其实都需要使用网络,所以在Android也统称为Network方式。
最后需要解释一点的是AGPS方式。很多人将它和基站定位混为一谈,但其实AGPS的本质仍然是GPS,只是它会使用基站信息对获取GPS进行辅助,然后还能对获取到的GPS结果进行修正,所以AGPS要比传统的GPS更快,准确度略高。
Android提供的定位接口
在写第一个程序之前,我对Android的幻想是这样的:提供了一个函数,能够让我直接从GPS模块中读取经纬度坐标,还有一个函数,能够直接访问网络,获得基站定位的结果。所以,我只需要调用调用函数就可以搞定这一切。
现实和理想总是有很大的差距。Android上的开发完全不是这么回事儿。前面提到过,GPS模块从启动到获取数据之间时间会比较长,可能有2~3分钟时间,所以,如果真有这么一个函数,那么你的程序可能会被这个函数阻塞几分钟。我想正是基于这样的考虑,android上要想获取定位信息,必须使用异步方式。
这是从网上随便摘一段。简单解释一下代码:
首先,你需要创建一个LocationManager;
然后定义出自己的LocationListener,LocationListener包涵了好几个成员函数,它们都是回调函数。最重要的一个是“onLocationChanged”,这个函数是在Android获取了新的location信息之后调用的,你可以在这个函数内来实现自己想要的功能。比如,你可以定义一个内部location变量,一旦这个函数被调用,就将内部location变量设置成最新的值;
最后,调用LocationManager.requestLocastionUpdates,它其实是将定义的locationListener注册到Android中。在上面的代码中,这句话是说让LocationListener监听GPS_PROVIDER的变化。GPS_PROVIDER对应于android上的GPS模块获取位置信息,还有一个NETWORK_PROVIDER表示通过network方式获取位置信息。
问题
那么接下来就有问题了,什么时候能够真正获得手机的 定位经纬度呢?等着onLocationChanged被调用吧。那它什么时候会被调用?没人知道。我写过一个小程序,测试Network方式下注册过 listener之后(requestLocationUpdates函数)和onLocationChanged被调用之间的时间间隔。测试的网络条件 很好。反复观察了几次,大部分都可以在几十毫秒内就返回了,但也有一些时候,时间间隔长达几十秒。这意味着,你的用户需要等上几十秒才能有返回。
所以,第一个需要注意的地方是,不要一直等待你的回调函数onLocationChanged被调用。你需要设置一个timeout机制。
这又会引入第二个问题。如果timeout了,但onLocationChanged仍然没有返回,怎么办?难道只能提示用户无法定位吗?
别急,Android还提供了一个函数:getlastKnowLocation。这个函数会返回android平台最后一次获取到的位置信息。比如,你可以这样:
- Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
所以,即便onLocationChanged没有被调用,我们仍然可以获取一个位置信息。当然,这里又引出了第三个问题:这个的返回值值得信赖吗?
如果用过一些LBS或者地图程序,你会发现有一个现象:在某些时候你打开地图结果被定位到的地方是你上一次使用地图程序的位置。这就是因为程序是采 用getLastKnownLocation获取的位置。这个问题的解决办法是,需要定义一个标准判断获取到的Location是否可信。Android的Location这个类除了包涵有latitude,longitude,还包含有很多其他的信息,比如何时获取到的,通过哪种方式获取到的,等等。程序员完全可以基于这些信息来判断获取到的Location是否过时或者是否可信。
合理的方案
最后,说一下整体方案。Android的官方文档【1】给出了推荐的方案:
首先注册自己的LocationListener,让它同时监听GPS_PROVIDER和NETWORK_PROVIDER;
然后可以调用getLastKnownLocation获得一个Location值,这个值可以作为一个备选值;
然后在一段用户可接受的时间内,不断接收从onLocationChanged返回的位置,并同之前的值做比较,选取其中的最佳;
最后,会剩下一个筛选后的最优结果,你需要判断这个结果是否可接受。如果可以接受,返回给用户,如果不行,告诉用户无法定位。
整个过程你需要定义两个重要的函数:一个是比较两个Location信息,返回其中好的那个;另一个函数则用来判断Location信息是否可以被接受。
package com.epapandroid.services;
import android.content.Context;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
public class Gps {
private Location location = null;
private LocationManager locationManager = null;
private Context context = null;
/**
* 初始化
*
* @param context
*/
public Gps(Context ctx) {
context = ctx;
locationManager = (LocationManager) context
.getSystemService(Context.LOCATION_SERVICE);
location = locationManager.getLastKnownLocation(getProvider());
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
1000, 1, locationListener);
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
1000, 1, locationListener);
}
// 获取Location Provider
private String getProvider() {
// 构建位置查询条件
Criteria criteria = new Criteria();
// 查询精度:高
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
// 是否查询海拨:否
criteria.setAltitudeRequired(false);
// 是否查询方位角 : 否
criteria.setBearingRequired(false);
// 是否允许付费:是
criteria.setCostAllowed(false);
// 电量要求:低
criteria.setPowerRequirement(Criteria.POWER_LOW);
// 返回最合适的符合条件的provider,第2个参数为true说明 , 如果只有一个provider是有效的,则返回当前provider
return locationManager.getBestProvider(criteria, true);
}
private LocationListener locationListener = new LocationListener() {
// 位置发生改变后调用
public void onLocationChanged(Location l) {
if (l != null) {
location = l;
}
}
// provider 被用户关闭后调用
public void onProviderDisabled(String provider) {
location = null;
}
// provider 被用户开启后调用
public void onProviderEnabled(String provider) {
Location l = locationManager.getLastKnownLocation(provider);
if (l != null) {
location = l;
}
}
// provider 状态变化时调用
public void onStatusChanged(String provider, int status, Bundle extras) {
}
};
//get Location
public Location getLocation() {
return location;
}
public void closeLocation() {
if (locationManager != null) {
if (locationListener != null) {
locationManager.removeUpdates(locationListener);
locationListener = null;
}
locationManager = null;
}
}
}
package com.epapandroid.services;
import java.math.BigDecimal;
import android.app.Service;
import android.content.Intent;
import android.location.Location;
import android.os.IBinder;
public class GpsService extends Service {
private Gps gps = null;
private boolean threadDisable = false;
Location location = null;
Double dbLatitude = 0.0;
Double dbLongitude=0.0;
@Override
public void onCreate() {
super.onCreate();
gps = new Gps(GpsService.this);
new Thread(new Runnable() {
@Override
public void run() {
while (!threadDisable) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (gps != null) { // 当结束服务时gps为空
// 获取经纬度
location = gps.getLocation();
// gps无法获取经纬度
if (location == null) {
System.out.println("----------->gps location null");
// 发送广播
Intent intent = new Intent();
intent.putExtra("lat",0.0);
intent.putExtra("lon",0.0);
intent.setAction("com.epapandroid.services.GpsService");
sendBroadcast(intent);
}
else {
// 发送广播
Intent intent = new Intent();
BigDecimal bdla = new BigDecimal(location.getLatitude());
dbLatitude = bdla.setScale(7,BigDecimal.ROUND_HALF_UP).doubleValue();
BigDecimal bdlo = new BigDecimal(location.getLongitude());
dbLongitude = bdlo.setScale(7,BigDecimal.ROUND_HALF_UP).doubleValue();
intent.putExtra("lat",
dbLatitude == 0.0 ? 0.0 : dbLatitude);
intent.putExtra("lon",
dbLongitude == 0.0 ? 0.0 : dbLongitude);
intent.setAction("com.epapandroid.services.GpsService");
System.out.println("GpsService----->"+dbLatitude);
sendBroadcast(intent);
}
}
}
}
}).start();
}
@Override
public void onDestroy() {
threadDisable = true;
if (gps != null) {
gps.closeLocation();
gps = null;
}
super.onDestroy();
}
@Override
public IBinder onBind(Intent arg0) {
return null;
}
}