一、
针对android项目中加速度传感检测手机方位,运行过程会时刻记录手机GPS的SRN详细数据,实时显示当前获取的卫星的PID和C/N,实时显示手机终端系统时间的需求,对于启动后台服务采集gps数据同时要把数据实时保存到本地文件中,要在gps状态改变时更新gps信息到界面上显示并且记录下来保存到本地的实时性冲突,对频繁读写文件还有界面更新不能让数据出现时间不连续。
在GPS项目的初始代码中逐步发现无法进行定时器更新执行,对界面显示数据的分离问题还有获取卫星列表失败、数据保存不连续等问题,造成获取gps信息不正常。
二、
在定时器无法更新执行任务的问题中原来代码使用AlarmManager服务,但是在设定重复任务的时候发现设定好了interval时间后,任务并没有按期望启动。从API 19开始,alarm的机制都是非准确传递,操作系统将会转换闹钟,来最小化唤醒和电池使用。有一些新的API会支持严格准确的传递,见setWindow(int, long, long, PendingIntent)和setExact(int, long, PendingIntent)。targetSdkVersion在API 19之前应用仍将继续使用以前的行为,所有的闹钟在要求准确传递的情况下都会准确传递。setRepeating方法在API 19开始将会失去原来的效果。可以换个定时器timer来使用重复执行任务,或者重复执行循环定时AlarmManager的设置。对于界面中时间显示和gps数据更新不在一个流程下,可以进行模块分离,单独更新对应控件显示。获取卫星列表信息失败,没有从gps状态监听器中获取,造成数据获取异常。需要在GpsStatus监听器的onGpsStatusChanged回调函数中的当卫星状态改变时来获取gps信息。获取gps信噪比数据不连续是因为服务被系统优化给杀死了,得保证采集数据的服务常驻不被系统停止。
三、
GPS是Android系统中重要的组成部分,通过它可以衍生出众多的与位置相关的应用。Android的GPS有一个专门的管理类,称为LocationManager,所有的GPS定位服务都由其对象产生并进行控制。首先需要明确的是,LocationManager类的对象获取并不是直接创建的,而是由系统提供的,具体来说,通过如下方法,为一个LocationManager对象建立一个对象引用:
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
至此,我们可以用locationManager这个对象对任意有关GPS的功能进行操作了。下表列出了几个常用的成员方法:getAllProviders获取所有与设备关联的定位模块的列表;getBestProvider获取设定的标准(Criteria对象)中最适合的一个设备;getGpsStatus获取GPS当前状态;getLastKnownLocation获取最近一次的可用地点信息;isProviderEnabled判断参数所提及的设备是否可用。
GPS还有一个支持API,即Location,它的作用是一个代表位置信息的抽象类,用它可以获取所有的位置数据。我们可以用以上的方法开始进行定位,可以将地点信息传递给一个Location对象:
Locationlocation = locationManager.getLastKnownLocation (LocationManager.GPS_PROVIDER);
我们还可以调用以下函数,对每次更新的位置信息进行我们想要的操作:
locationManager.requestLocationUpdates (LocationManager.GPS_PROVIDER,1000, 10, new LocationListener())
其中,第一个参数是LocationProvider对象,第二个参数是刷新的时间差,这里设定为1秒,第三个参数是位置差,这里设定为10米,第四个参数为一个位置监听器对象,可以重写这些方法来实现我们的需求。
大致流程就是首先通过LocationMangager位置管理器,要想操作定位相关设备,必须先定义个LocationManager。可以通过如下代码创建LocationManger对象。然后通过LocationListener位置监听器,可以监听位置变化,监听设备开关与状态。Location位置信息,通过Location可以获取时间、经纬度、海拔等位置信息。采用locationListener里面的onLocationChanged()来获取location,也可以通过locationManager获取getLastKnownLocation主动获取location。GpsStatus.Listener来实现GPS状态监听,包括GPS启动、停止、第一次定位、卫星变化等事件。GpsStatus对应GPS状态信息,在卫星状态变化时就用到了GpsStatus。GpsSatellite对应定位卫星,包含卫星的方位、高度、伪随机噪声码、信噪比等信息。
项目中之前使用了AlarmManager来进行定时提醒的功能。当时开发这个功能的时候并没有发现会有这种Bug,原因是开发时只会预约一个来测试功能是否有实现,实现了就算完成,而这个问题恰好是有多个预约时才会发生的问题。发现其中会有一两个通知并没有按照我设定的时间唤醒设备,而是会和与他时间相近的下一个预约同时出现(也就是发生了时间不精确的问题)。当时发现这个问题时以为是我在传递预约时间时发生了问题,就打Log查看了一下,发现并没有什么问题,既然时间没有问题,那很有可能是AlarmManager的问题。
官方文档说从API19(android4.4)开始,为了节能省电(减少系统唤醒和电池使用)。使用Alarm.set()和Alarm.setRepeating()已经不保证精确性。其实AlarmManager就是用的AIDl通信,从Alarm对象的获取我们也可以看出来getSystemService对应的ALARM_SERVICE是获取的系统服务,AlarmManager真正的实现都在FrameWork层的AlarmManagerService这个服务中。普通的set()和setRepeating()方法会通过调用legacyExactLength()这个方法返回这个值:
private long legacyExactLength(){
return (mAlaysExact ? WINDOW_EXACT : WINDOW_HEURISTIC);
}
mAlwaysExact这个变量,如果是小于API19的版本会使用WINDOW_EXACT参数,这个参数是0(意思就是区间设置为0,那么就会按照triggerAtMillis这个时间准时触发,也就是精准触发)另一个参数WINDOW_HEURISTIC的值是-1,这个值具体的用法就要看AlarmManagerService具体的实现了,反正只要知道这个值是不精准就可以。剩下的其他set方法直接就将这个值设置为WINDOW_EXACT,那么那几个方法就都是精准通知了,除了setInexactRepeating方法将参数设置为WINDOW_ HEURISTIC。只需要不同方法传递不同参数就可以了。setAlarmClock这个方法用的是WINDOW_EXACT这个参数,所以这个方法也是精准的,不过他有一些局限性:只能在API21版本和之后的版本调用并且Type是固定RTC_WAKEUP。
如果没有设置和使用精准通知的话,系统会把触发时间相近的Alarm放在同一个batch中,然后每个bach根据时间排序放在mAlarmBatchs中,前面的就是先要触发的alarm。也就是Alarm会分为一批一批的一起触发,而不是每个Alarm都要触发。系统中有多个Alarm时会发生时间不准的现象,但如果系统中只有几个Alarm,并且他们的触发时间隔得很远,那么Alarm就自己分为一批,触发还是会准的。
从API 19开始,alarm的机制都是非准确传递,操作系统将会转换闹钟,来最小化唤醒和电池使用。有一些新的API会支持严格准确的传递,见setWindow(int, long, long, PendingIntent)和setExact(int, long, PendingIntent)。argetSdkVersion在API 19之前应用仍将继续使用以前的行为,所有的闹钟在要求准确传递的情况下都会准确传递。setRepeating方法在API 19开始将会失去原来的效果,所以要判断系统sdk版本进行选择:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//参数2是开始时间、参数3是允许系统延迟的时间
am.setWindow(AlarmManager.RTC, timeInMillis, interval, sender);
} else {
am.setRepeating(AlarmManager.RTC, timeInMillis, interval, sender);
}
因为setWindow只执行一次,所以要重新定义闹钟实现循环。在广播接收的地方实现无限循环:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
setAlarmTime(context, System.currentTimeMillis() + 15, "自定义action", 15);
}
或者采取另一种定时器方案:利用Timer的schedule方法重复执行任务。Timer是一种定时器工具,用来在一个后台线程计划执行指定任务。它可以计划执行一个任务一次或反复多次。TimerTask一个抽象类,它的子类代表一个可以被Timer计划的任务。
用Timer线程实现和计划执行一个任务的基础步骤:实现自定义的TimerTask的子类,run方法包含要执行的任务代码。实例化Timer类,创建计时器后台线程。实例化任务对象(new RemindTask())。制定执行计划用schedule方法,第一个参数是TimerTask对象,第二个参数表示开始执行前的延时时间。还有一种方法可以指定任务的执行时间。当 计划反复执行的任务时,如果你注重任务执行的平滑度,那么请使用schedule方法,如果你在乎的是任务的执行频度那么使用scheduleAtFixedRate方法。 例如,这里使用了schedule方法,这就意味着所有beep之间的时间间隔至少为1秒,也就是说,如 果有一个beap因为某种原因迟到了(未按计划执行),那么余下的所有beep都要延时执行。如果我们想让这个程序正好在3秒以后终止,无论哪一个beep因为什么原因被延时,那么我们需要使用scheduleAtFix edRate方法,这样当第一个beep迟到时,那么后面的beep就会以最快 的速度紧密执行(最大限度的压缩间隔时间)。
mRecordTimer = new Timer();
mRecordTimer.schedule(new ScheduleTask ("com.exams.demo10_lbs.catch", LBSService. this ), 0L, 1000L);
public class ScheduleTask extends TimerTask
{
Context mContext;
String mSchedule;
public ScheduleTask(String paramString, Context paramContext)
{
this.mSchedule = paramString;
this.mContext = paramContext;
}
public void run()
{
Intent localIntent = new Intent(this.mSchedule);
this.mContext.sendBroadcast(localIntent);
}
}
对于界面中控件刷新时间和gps数据分离,只需要将原来在服务中一起处理的时间信息计时使用主界面的设置timer定时器来控制刷新界面时间就好了。
定位服务的初始化设置步骤后,但GPS卫星是定期广播数据的,也就是说会定期收到卫星的GPS数据。我们并不能跟卫星主动申请数据,只能被动接收数据。(因此我们需要注册一个监听器来处理卫星返回的数据。Android下提供了GpsStatus这个类来看卫星信息,通过调用此类的一个method getSatellites(),可以得到接收到的卫星的信息列表Iterable<GpsSatellite>。当然这些操作也是在一个listener当中来做的:GpsStatus.Listener。GpsStatus的listener也是注册于locationManager。
所以不能在主线程中使用locationManager的getGpsStatus函数直接获取卫星信息,需要在GPS状态监听器GpsStatus.Listener的回调函数onGpsStatusChanged中来监听包括GPS启动、停止、第一次定位、卫星变化等事件。
//定义
GpsStatus.Listener gpsStatusListener=new GpsStatus.Listener(){
public void onGpsStatusChanged(int event) {
if(event==GpsStatus.GPS_EVENT_FIRST_FIX){
//第一次定位
}else if(event==GpsStatus.GPS_EVENT_SATELLITE_STATUS){
//卫星状态改变
GpsStatus gpsStauts= locationManager.getGpsStatus(null); //取当前状态
int maxSatellites = gpsStauts.getMaxSatellites(); //获取卫星颗数的默认最大值
Iterator<GpsSatellite> it = gpsStatus.getSatellites().iterator();//创建一个迭代器保存所有卫星
int count = 0;
while (it.hasNext() && count <= maxSatellites) {
count++;
GpsStatellite s = it.next();
}
system.out.println("搜索到:"+count+"颗卫星");
}else if(event==GpsStatus.GPS_EVENT_STARTED){
//定位启动
}else if(event==GpsStatus.GPS_EVENT_STOPPED){
//定位结束
}
}
};
//绑定
locationManager.addGpsStatusListener(gpsStatusListener);
对于频繁文件读写操作造成性能卡顿、界面更新延缓数据出现间隔就需要将数据处理、数据写文件保存、数据界面刷新的逻辑流程进行分离,首先每隔一秒采集数据并且保存到缓存变量,另外起定时器每隔一秒读取缓存变量的数据来刷新界面。为了避免频繁读写文件,需要将一段时间间隔的数据进行拼接保存,在一段比较长的时间间隔后再操作IO把数据写入文件。以下是广播接收器对几种操作流程的动作分离Action:
cmdReceiver = new CommandReceiver();
IntentFilter filter = new IntentFilter();// 创建IntentFilter对象
filter.addAction("com.exams.demo10_lbs.stopDemo");
filter.addAction("com.exams.demo10_lbs.catch");
filter.addAction("com.exams.demo10_lbs.refresh");
filter.addAction("com.exams.demo10_lbs.write");
filter.addAction("com.exams.demo10_lbs.startDemo");
分别对采集数据、刷新界面、数据写入文件的三种定时器控制:
LBSService.this.mRecordTimer = new Timer();
LBSService.this.mRecordTimer.schedule(new ScheduleTask("com.exams.demo10_lbs.catch", LBSService.this), 1000L, 1000L);
LBSService.this.mInterfaceRefreshTimer = new Timer();
LBSService.this.mInterfaceRefreshTimer.schedule(new ScheduleTask("com.exams.demo10_lbs.refresh", LBSService.this), 1100L, 1000L);
LBSService.this.mDataWriterTimer = new Timer();
LBSService.this.mDataWriterTimer.schedule(new ScheduleTask("com.exams.demo10_lbs.write", LBSService.this), 6000L, 40000L);
对于服务被系统杀死,就需要提高服务的优先级或者设置杀死后再自动循环重启。当然对于系统休眠调整耗电引起的进程优化,那就需要保证系统不进入休眠,保证系统awake。
Android系统会尽量保持拥有service的进程运行,只要在该service已经被启动(start)或者客户端连接(bindService)到它。当内存不足时,需要保持,拥有service的进程具有较高的优先级。还有在服务service的onStartCommand方法,返回START_STICKY来保证service不被杀掉。START_STICKY在运行onStartCommand后service进程被kill后,那将保留在开始状态,但是不保留那些传入的intent。不久后service就会再次尝试重新创建,因为保留在开始状态,在创建service后将保证调用onstartCommand。如果没有传递任何开始命令给service,那将获取到null的intent。提升service优先级在AndroidManifest.xml文件中对于intent-filter可以通过android:priority = "1000"这个属性设置最高优先级,1000是最高值,如果数字越小则优先级越低,同时适用于广播。
<service
android:name="com.dbjtech.acbxt.waiqin.UploadService"
android:enabled="true" >
<intent-filter android:priority="1000" >
<action android:name="com.dbjtech.myservice" />
</intent-filter>
</service>
提升service进程优先级Android中的进程是托管的,当系统进程空间紧张的时候,会依照优先级自动进行进程的回收。Android将进程分为6个等级,它们按优先级顺序由高到低依次是:前台进程、可视进程、次要服务进程、后台进程、内容供应节点、空进程。当service运行在低内存的环境时,将会kill掉一些存在的进程。因此进程的优先级将会很重要,可以使用startForeground将service放到前台状态。这样在低内存时被kill的几率会低一些。
在onStartCommand方法内添加如下代码:
Notification notification = new Notification(R.drawable.ic_launcher,
getString(R.string.app_name), System.currentTimeMillis());
PendingIntent pendingintent = PendingIntent.getActivity(this, 0,
new Intent(this, AppMain.class), 0);
startForeground(0x111, notification);
当然也可以onDestroy方法里重启service:
public void onDestroy() {
Intent sevice = new Intent(this, MainService.class);
this.startService(sevice);
super.onDestroy();
}
看Android的文档知道,当进程长期不活动,或系统需要资源时,会自动清理门户,杀死一些Service,和不可见的Activity等所在的进程。但是如果某个进程不想被杀死(如数据缓存进程,或状态监控进程,或远程服务进程),可以Application加上Persistent属性:
<application
android:name="XXX"
android:label="@string/app_name"
android:persistent="true">
</application>
对于系统进入休眠,Android系统为了省电以及减少CPU消耗,在一段时间后会使系统进入休眠状态,这时Android系统中CPU会保持在一个相对较低的功耗状态,会将一些不在活动或者较为耗内存的应用优化。Wake Lock是一种锁的机制,只要有人拿着这个锁,系统就无法进入休眠,可以被用户态程序和内核获得。这个锁可以是有超时的 或者 是没有超时的,超时的锁会在时间过去以后自动解锁。如果没有锁了或者超时了,内核就会启动休眠的那套机制来进入休眠。所有的锁必须成对的使用, 如果申请了而没有及时释放,会造成系统故障。如申请了partial wakelock,而没有及时释放,那系统就永远进不了Sleep模式。
mWakeLock = mPowerManager. newWakeLock( PowerManager.PARTIAL_ WAKE_LOCK , "grow lock");
mWakeLock.setReferenceCounted(false);
...
if(mWakeLock!=null &&!mWakeLock.isHeld())
{
mWakeLock.acquire(40000);
}
...
if(mWakeLock!=null){
mWakeLock.release();
mWakeLock=null;
另外WakeLock的设置是Activiy级别的,不是针对整个Application应用的。所以application下有多个activity一定需要注意下。要进行电源的操作需要在AndroidManifest.xml中声明该应用有设置电源管理的权限。
四、
通过测试对修改后apk的运行采集数据效果来看,使用启动GPS实时获取监听requestLocationUpdates的系统自带定时设置就可以定时获取新的卫星信息,使用AlarmManager的setWindow或者设置timer循环定时器就可以避免定时执行任务失败的现象。使用mvc流程分离后对数据的采集保存不再出现不连续的问题,加了对应保证服务不被系统优化的措施后,采集执行任务就不会中断了。