前言:
在移动客户端的开发中,地理位置定位是一个非常重要的环节,有些时候用户可能会限制web app或者Android app的一些权限,或者由于信号不佳的原因无法获得准确的GPS位置,甚至为了省电,用户可能对开启GPS开关可能会有抵触情绪。但是不能因为GPS的种种限制就放弃了对用户位置的追踪。要通过一切能发送出信号的物体尽可能准确的获取到用户的位置,有时可以牺牲一些精度,对于大数据和用户地区分布分析来说,有一个大体的位置已经够分析人员使用,而且绕开GPS的重重壁垒,为数据的完整性提供可靠方案
开发背景:
有些项目经理反应目前版本的app耗电量过大,状态栏的GPS标识一直在闪烁。于是为了降低软件的耗电量,同时保证定位的经纬度,并且不能仅依赖GPS进行定位,在用户不愿意开启GPS开关时,需要通过手机附近的基站,wifi热点,甚至IP地址进行定位。对提高用户体验有非常重要的作用。(之前的获取经纬度策略是使用Android提供的API,通过开启一个IntentService,在Service中开始一个死循环的子线程不停的进行GPS位置获取,相关代码可以看这里《使用Andorid原生工具类获取手机经纬度》)
实现效果:
实现的效果可以参考高德地图,高德地图的使用体验我觉得是不错的,甚至完全不需要开启GPS开关,就可以准确定位到道路街道,误差一般不会超过50米,我猜测高德的实现方案无非也是wifi加基站。我做过一次测试,在一个收不到GPS信号的地下室中(但是附近有热点和基站),在非联网状态下打开高德地图,高德会把我定位到上次成功的位置,当我开启了网络开关,马上位置就八九不离十了。
最近整理成了demo,下面是截图
该demo的github地址,希望能和其他大神一起优化这个定位模块,变得更精确,更省电:https://github.com/AlexZhuo/AlxLocationManager
定位原理:
原理其实很简单,在GPS定位没有成功时(GPS定位一般是最准确的,具有最高的优先级),用手机扫描附近的基站,记录下附近基站的mcc,lac,cid等参数,用于识别该基站,通过数据库查询到该基站的GPS位置,用收到该基站的信号强度当做相对基站的距离,如果搜索到附近的基站比较多,就可以以基站的位置为圆心,以信号强度为半径进行画圆,多个基站画出圆圈重合的部分就是当前最可能的位置。同理wifi热点定位也是一样,不过用于识别热点的换成了MAC地址,一样用信号的强度作为距离的表示。
定位策略:
1、首先在开启app的时候,取系统中记录的最近一次GPS定位位置,如果取得的GPS精确度小于200m,那么就弃用,如果取到了正常的结果,就不需要位置监听和基站、wifi定位了,但是这在多数情况下是取不到的,尤其是刚刚打开GPS开关没多久的时候,这时候从SharedPreference里取出上次app定位成功的地点先顶上。
2、如果上一步没有马上取得当前的GPS位置,就会开启一个监听不断监测位置的变化,然而这个监听的要求是高灵敏度,高精度,高耗电的,此时状态栏的GPS标识会开始闪烁,直到取得第一次精确的GPS位置,如果取得的GPS精确度小于200m,那么就弃用。
3、在开启监听器的同时,开始扫描手机附近的基站信息和wifi热点信息,获取每个基站和热点的信号强度,通过数据库查询出每个基站和热点的GPS信息,然后通过相关数学算法求出手机的大致经纬度(在本文中这些是通过谷歌提供的API进行实现)但是如果取得的GPS精确度小于200m,那么就弃用。
4、监听器获取到当前GPS定位成功后,就关掉当前的监听器,然后开启一个策略不同的监听器,新监听器的主要特点是低精度,低灵敏度,低耗电,此时状态栏的GPS闪烁停止,但是定位的精度大幅降低,可能会跑到好几公里以外。
5、为了防止监听器后台监听耗电,被认为成后台偷跑电量的程序,需要在程序放到后台和关闭的时候关闭响应的监听。
6、渠道优先级:GPS>基站>WIFI热点>IP,这只是一个大体的安排,实际上在GPS信号不太好的地方,wifi加基站的定位结果可能要比GPS准确的多,所以具体采用哪种定位方式需要看他们的accuracy值
7、对于一些国产电信公司定制机,一般没有安装谷歌框架的,使用谷歌原生框架获取GPS位置,基站定位,wifi热点定位不变。
能正常定位的情景:
1、如果用户在开阔的室外(GPS开,wifi可关可开,sim卡可以不插,可以不联网),并且天气晴朗,一般可以收到良好的GPS信号,此时的定位结果应是GPS定位信号
2、如果用户在封闭的室内,且无法收到GPS信号,此时手机必须要联网才能获得定位信息,而且一下条件要二选一(1)手机要插sim卡且有信号(2)手机开启wifi开关且附近有热点。如果两种信号都收不到,那么会根据ip进行定位,且要在手机没有开VPN网络的时候
无法定位的情况:
1、用户在封闭的室内,且没有联网。但此时会采用SharePreference中的记录
2、用户在室外,有大雨,大雾,阴天,且没有联网。或者联网了但手机没有信号,附近没有wifi
运行环境:
1、首先需要引入Google Service gcm location SDK,引入这个SDK的原因是:省电。并且防止状态栏上的GPS标识一直闪烁,谷歌的官方文档说的很直白,不推荐使用Android原生的接口,原文如下
the Google Play services location APIs are preferred over the Android framework location APIs (android.location) as a way of adding location awareness to your app. If you are currently using the Android framework location APIs, you are strongly encouraged to switch to the Google Play services location APIs as soon as possible.
该SDK的官方文档地址:https://developer.android.com/training/location/index.html
引入该SDK需要在gradle文件里添加
compile 'com.google.android.gms:play-services-location:8.4.0'
注意:许多国产手机,尤其是运营商定制机里往往没有内置谷歌框架,此时的表现是一直无法connect GPS,这时候就不能用谷歌的SDK,而需要使用Android原生API,也就是《使用Andorid原生工具类获取手机经纬度》这篇文章中介绍的方法
2、开启相关权限
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
注意:相关权限一旦用户没有授予或授予后驳回,可能会有crash的情况
主要的暴露方法:
void onCreateGPS(Application context)用于开启一次gps位置获取,程序会先获取最近一次的GPS位置,如果失败的话就开启GPS追踪和基站,wifi定位等
void restartGPS(Application context)在进入一些对GPS要求比较高的页面时,重新获取当前的准确位置的方法,本方法会先暂停掉当前的GPS追踪,然后重新获取一遍位置
void stopGPS()在退出app时,为了节省电力而彻底关闭GPS追踪
void pauseGPS()当程序放到后台时为了防止后台耗电而停止GPS跟踪
GPS的后台省电:
要监听app是否被放到了后台,Android系统没有给出相关接口或广播,只能通过Activity的Stop和Start的关系来判断,一般来说,一个Stop后如果紧跟着一个start那么就可以说这是内部的转换,但是如果只Stop没有start那么就很有可能是放到后台了。
第二种监听方法是,在Activity stop的时候观察一下本Activity是否还在栈顶,如果还在栈顶那么可以说明被放到后台了,下面这段代码就是这个思想,需要程序里用到的Activity基本都以这个BaseActivity为父类
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import com.demo.AlxLocationManager;
import java.util.List;
public class BaseActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onStop() {
super.onStop();
if (!isAppOnForeground(this)) {
//app 进入后台
JLogUtils.i("AlexLocation","程序进入后台运行");
//全局变量isActive = false 记录当前已经进入后台
isRunningBackGround = true;
AlxLocationManager.pauseGPS();
}
}
@Override
protected void onRestart() {
super.onRestart();
//从后台恢复,如果还没有很好的获得经纬度就继续获得
if(isRunningBackGround && AlxLocationManager.manager!=null && AlxLocationManager.manager.currentStatus!= AlxLocationManager.STATUS.TRYING_FIRST)AlxLocationManager.onCreateGPS(getApplication());
isRunningBackGround = false;
}
/**
* 程序是否在前台运行
*
* @return
*/
public static boolean isAppOnForeground(Activity activity) {
// Returns a list of application processes that are running on the
// device
ActivityManager activityManager = (ActivityManager) activity.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
String packageName = activity.getApplicationContext().getPackageName();
List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager
.getRunningAppProcesses();
if (appProcesses == null)
return false;
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
// The name of the process that this object is associated with.
if (appProcess.processName.equals(packageName)
&& appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true;
}
}
return false;
}
}
谷歌API相关:
谷歌那里有全世界几乎所有基站的GPS位置,而且谷歌的数据库里还有全世界许多wifi热点的GPS位置,还有IP的归属地,所以对于我现在的支援东南亚国家的app来说使用谷歌的数据库再合适不过了。
谷歌API的主页:https://developers.google.com/maps/documentation/geolocation/intro
1、首先先申请一个密钥,申请非常简单,只要有一个谷歌账号,点一下“申请密钥”,几下就好了
2、使用postman等测试工具,使用谷歌给出的json示例看看好不好用
{
"homeMobileCountryCode": 310,//中国一般是460
"homeMobileNetworkCode": 260,//使用手机获取
"radioType": "gsm",//这个填起来要特别小心,宁愿不填,如果填错了的话有可能无法获得定位信息
"carrier": "T-Mobile",//运营商,作用不大
"cellTowers": [//基站列表
{
"cellId": 39627456,//必填项,比较重要
"locationAreaCode": 40495,
"mobileCountryCode": 310,//中国是460
"mobileNetworkCode": 260,
"age": 0,
"signalStrength": -95//相当于距离
}
],
"wifiAccessPoints": [//wifi热点
{
"macAddress": "01:23:45:67:89:AB",
"signalStrength": 8,
"age": 0,
"signalToNoiseRatio": -65,
"channel": 8
},
{
"macAddress": "01:23:45:67:89:AC",
"signalStrength": 4,
"age": 0
}
]
}
post接口地址:
https://www.googleapis.com/geolocation/v1/geolocate?key=YOUR_API_KEY
这里要注意一下,以前google也提供了一个类似的接口,但是从2012年开始就弃用了,很多老博客里还写着这个过时的接口,在江湖上流传已久,欺骗了不