在android端做即时消息的时候,遇到的坑点是怎么保证消息即时性,又不耗电。为什么这么说呢?
原因是如果要保证消息即时性,通常有两种机制pull或者push。pull定时轮询机制,比较浪费服务器资源;push服务器推送机制,需要保持长连接,客户端和服务器都要求比较高(网络环境,服务器保持连接数等),它们的详细优缺点不描述了。上面这两种机制都要求客户端长期处于活动状态,前提是cpu处于唤醒状态,而android端有休眠机制,保证手机在大部分时间里都是处于休眠,降低耗电量,延长机时间。手机休眠后,线程处理暂停状态,这样前面说的两种方式,都会处于暂停状态,从而导致休眠后就无法收消息问题。可能有人说手机有唤醒机制,如果一直唤醒呢,这样导致做的软件是耗电大户,基本不要一天手机电量就被干光,想想睡觉前有半格电,早上起来电量干光被关机,郁闷的心情顿时油然而生,所以这样干是不行的,会直接导致软件被卸载。
即时与耗电比较矛盾,怎么办呢?解决办法就是平衡了,保证即时性的同时又尽量降低耗电。
一、唤醒机制
手机有休眠机制,它也提供了唤醒机制,这样我们就可以在休眠的时候,唤醒我们的程序继续干活。关于唤醒说两个类:AlarmManager和WakeLock:
AlarmManager手机的闹铃机制,走的时钟机制不一样,确保休眠也可以计时准确,并且唤醒程序,具体用法就不说了,AlarmManager能够唤醒cpu,将程序唤醒,但是它的唤醒时间,仅仅确保它唤醒的意图对象接收方法执行完毕,至于方法里面调用其他的异步处理,它不保证,所以一般他唤醒的时间比较短,做完即继续休眠。如果要确保异步之外的事情做完,就得申请WakeLock,确保手机不休眠,不然事情干得一半,手机就休眠了。
这里使用AlarmManager和WakeLock结合的方式,把收消息放在异步去做,具体怎么做后面再看。先说说闹铃唤醒周期问题,为确保消息即时,当然是越短越好,但是为了确保省电,就不能太频繁了。
策略一、可以采用水波策略,重设闹铃:开始密集调度,逐渐增长。如:30秒开始,每次递增5秒,一直递增到25分钟,就固定周期。
策略二、可以采用闲时忙时策略,白天忙,周期密集,晚上闲时,周期长。
策略三、闹铃调整策略,确保收消息即时,到收到消息时,就重新初始化那闹铃时间,由最短周期开始,确保聊天状态下,即时。
策略四、WakeLock唤醒,检测手机屏幕是是否亮起,判断是否需要获取唤醒锁,降低唤醒次数。
1、设置闹铃
am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, (triggerAtTime + time), pi);
2、闹铃时间优化
public class AlarmTime { public static final AtomicLong alarmTime=new AtomicLong(0); /** * 初始化闹铃时间,重连或者收到消息初始化一下 */ public static long initAlarmTime(){ alarmTime.set(Global.ALARM_TRIGGER_TIME); return alarmTime.get(); } /** * 优化闹铃时间,重连错误数超过一定次数,优化闹铃时间再尝试重连到错误数 * 10分钟,30秒、30秒、;;;;到达错误数,10分钟 ;;;;; * @return */ public static long optimizeAlarmTime(){ alarmTime.set(Global.ALARM_TRIGGER_OPTIMIZE_TIME);//10分钟 return alarmTime.get(); } public static long incrementTime(){ long time =alarmTime.get(); if(time==0) return alarmTime.addAndGet(Global.ALARM_TRIGGER_TIME);//默认30秒开始 else if(time<Global.ALARM_TRIGGER_MAX_TIME)//25分钟 return alarmTime.addAndGet(Global.ALARM_TRIGGER_TIME_INCREMENT);//每次递增5秒 else return time; } }
3、唤醒机制
public final class IMWakeLock { private static final String TAG = IMWakeLock.class.getSimpleName(); private WakeLock wakeLock = null; private String tag=""; private PowerManager pm; public IMWakeLock(Context paramContext,String tag){ this.tag =tag; pm= ((PowerManager) paramContext. getSystemService(Context.POWER_SERVICE)); wakeLock = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK , tag); } /** * 获取电源锁,保持该服务在屏幕熄灭时仍然获取CPU时,保持运行 */ public synchronized void acquireWakeLock() { if(!pm.isScreenOn()) { if (null != wakeLock&&!wakeLock.isHeld()) { ImLog.d(TAG, tag+"@@===>获取唤醒休眠锁"); wakeLock.acquire(); } } } /** * 释放设备电源锁 */ public synchronized void releaseWakeLock() { if (null != wakeLock && wakeLock.isHeld()) { ImLog.d(TAG, tag+"@@===>释放唤醒休眠锁"); wakeLock.release(); } } public synchronized void finalize(){ if (null != wakeLock && wakeLock.isHeld()) { ImLog.d(TAG, tag+"@@===>释放唤醒休眠锁"); wakeLock.release(); } wakeLock = null; } public boolean isScreenOn(){ return pm.isScreenOn(); } }
4、唤醒时机
private void startApNotify(){ if(this.sessionID==0||this.ticket==null) return; if(wakeLock.isScreenOn()){ ImLog.d(TAG, "[email protected]@===>启动空请求"); apNotifyThread=new ApNotifyThread(this,false); }else{ wakeLock.acquireWakeLock(); apNotifyThread=new ApNotifyThread(this,true); } exec=Executors.newSingleThreadExecutor(); exec.execute(apNotifyThread); exec.shutdown(); }
唤醒机制想好了,但是如果唤醒后,长时间不释放唤醒锁也不行,所以这里就得考虑收消息机制。
二、消息收取
消息收取,采用push与pull结合方式,为什么采用两种结合方式呢?先看看特性
push:即时,维持连接,耗时长。
pull:被动,维持连接,处理时间短。
根据手机的唤醒和休眠机制,可以分析出push适合手机在位休眠的时候,未休眠,保持长连接,确保消息即时收取。而pull适合手机休眠状态(休眠状态没有办法判断,只能根据屏幕亮起否判断,曲线救国了),也就是休眠后,用唤醒机制唤醒,pull下有没有消息,没有消息释放休眠锁,有消息收取消息,收取完后释放休眠锁,确保唤醒时间最短,降低耗电量。
push逻辑流程图:
pull逻辑流程图:
代码处理部分:
public class ApNotifyThread extends Thread{ private static final String TAG = ApNotifyThread.class.getSimpleName(); protected volatile boolean isRunning=false; protected volatile APHold.Client client; protected volatile VRVTHttpClient thc; protected volatile TProtocol protocol; protected volatile long sessionID; protected volatile String ticket; protected final long ERRORNUM=15; protected NotifyService service; protected boolean isOld=false; protected boolean isDoShortRequest=false; public ApNotifyThread(NotifyService service,boolean isDoShortRequest){ this.sessionID=service.getSessionID(); this.ticket=service.getTicket(); this.service=service; this.isDoShortRequest=isDoShortRequest; } @Override public void run(){ ImLog.d(TAG, "[email protected]@===>空请求开始处理 threadID="+Thread.currentThread().getId()); this.isRunning=true; if(this.isDoShortRequest){ if(shortEmptyRequest()&&this.isRunning) longEmptyRequest(); //再开启长空请求 }else{ longEmptyRequest(); } ImLog.d(TAG, "[email protected]@===>"+(this.isOld?"上一个":"")+"空请求终止 threadID="+Thread.currentThread().getId()); this.isRunning=false; } /** * 初始化 * @param isLongTimeOut * @throws Exception */ private void init(boolean isLongTimeOut) throws Exception{ thc= NotifyHttpClientUtil.getVRVTHttpClient(isLongTimeOut); protocol = new TBinaryProtocol(thc); } /** * 长空请求 */ private void longEmptyRequest(){ try{ this.init(true); client= new APHold.Client(protocol); for (;;) { if(!NetStatusUtil.havActiveNet(IMApp.getApp())){ ImLog.d(TAG, "[email protected]@===>无可用网络"); break; } try { if(!handleMessage()) break; } catch (TException e) { if(!this.isRunning) break; ImLog.d(TAG, "[email protected]@===>发请求异常:"+ e.getMessage()); if(exceptionHandler(e)){ throw new IMException("连接失败次数过多",MessageCode.IM_EXCEPTION_CONNECT); } continue; } } ImLog.d(TAG, "[email protected]@===>"+(this.isOld?"上一个":"")+"空请求正常退出"); } catch (Exception e) { ImLog.d(TAG, "[email protected]@===>"+(this.isOld?"上一个":"")+"空请求异常退出"+e.getMessage()); if (exceptionHandler(e)) { // 调用重连 ImLog.d(TAG, "[email protected]@===>调用重连"); this.service.getDataSyncer().setValue(UserProfile.RECONNECT, "0"); } }finally{ close(); } } /** * 短空请求 * @return */ private boolean shortEmptyRequest(){ boolean isDoLongRequest=true; try{ long messageNum=0; if(!NetStatusUtil.havActiveNet(IMApp.getApp())){ ImLog.d(TAG, "[email protected]@===>无可用网络"); return false; } this.init(false); //获取消息数 APService.Client apclient = new APService.Client(protocol); this.service.getDataSyncer().setValue(UserProfile.LASTREQUESTTIME, String.valueOf(SystemClock.elapsedRealtime())); ImLog.d(TAG, "[email protected]@===>notifyID:"+NotifyID.notifyID.get()); messageNum= apclient.getNotifyMsgSize(sessionID, ticket, NotifyID.notifyID.get()); NotifyError.notifyErrorNum.set(0); ImLog.d(TAG, "[email protected]@===>获取消息条数:"+messageNum); if(messageNum==-1) throw new IMException("session 失效",MessageCode.IM_BIZTIPS_SESSIONINVAILD); //如果有消息接收消息 if(messageNum>0&&this.isRunning){ long receiveMessageNum=0; client= new APHold.Client(protocol); for (;;) { if(!NetStatusUtil.havActiveNet(IMApp.getApp())){ ImLog.d(TAG, "[email protected]@===>无可用网络"); break; } if(!handleMessage()) break; receiveMessageNum++; if(receiveMessageNum==messageNum) //短连接接收完后退出 break; } } ImLog.d(TAG, "[email protected]@===>"+(this.isOld?"上一个":"")+"空请求正常退出"); }catch(Exception e){ ImLog.d(TAG, "[email protected]@===>"+(this.isOld?"上一个":"")+"空请求异常退出"+e.getMessage()); if(exceptionHandler(e)){ isDoLongRequest=false; //调用重连 ImLog.d(TAG, "[email protected]@===>调用重连"); this.service.getDataSyncer().setValue(UserProfile.RECONNECT, "0"); } } finally{ close(); this.service.releaseWakeLock(); } return isDoLongRequest; } /** * 异常处理 判断是否重连 * @param e * @return */ private boolean exceptionHandler(Exception e){ boolean isReconnect=false; if ( e instanceof IMException) { isReconnect=true; }else if (!(e instanceof SocketTimeoutException)&&!(e instanceof NoHttpResponseException)) { NotifyError.notifyErrorNum.incrementAndGet(); if(NotifyError.notifyErrorNum.get()>this.ERRORNUM){ isReconnect=true; NotifyError.notifyErrorNum.set(0); } }else NotifyError.notifyErrorNum.set(0); e.printStackTrace(); return isReconnect; } /** * 空请求发送和接收数据处理 * @throws TException */ private boolean handleMessage() throws TException{ if(!this.isRunning) return false; ImLog.d(TAG, "handleMessage@@===>sessionID "+sessionID); SendEmptyRequestReq req = new SendEmptyRequestReq(); req.setSessionID(sessionID); req.setTicket(ticket); req.setNotifyID(NotifyID.notifyID.get()); ImLog.d(TAG, "[email protected]@===>一次空请求周期开始 "); this.service.getDataSyncer().setValue(UserProfile.LASTREQUESTTIME, String.valueOf(SystemClock.elapsedRealtime())); client.SendEmptyRequest(req); NotifyError.notifyErrorNum.set(0); if(!this.isRunning) return false; APNotifyImpl iface = new APNotifyImpl(); APNotify.Processor<Iface> processor = new APNotify.Processor<Iface>(iface); boolean isStop = false; while (!isStop) { try { ImLog.d(TAG, "[email protected]@===>进入接收数据处理"); while (processor.process(protocol, protocol) == true) { isStop = true; break; } ImLog.d(TAG, "[email protected]@===>结束接收数据处理"); } catch (TException e) { ImLog.d(TAG, "[email protected]@===>接收数据处理异常"); isStop = true; } } ImLog.d(TAG, "[email protected]@===>一次空请求周期结束"); if(!iface.isSessionVaild){//后台报session 失效 this.service.setSessionID(0); this.service.setTicket(null); return false; } //重设闹铃 this.service.getDataSyncer().setValue(UserProfile.ALARM_TTIME, "0"); return true; } /** * 关闭连接 */ private void close() { synchronized(this){ if (thc != null) { thc.shutdown(); thc.close(); thc=null; } } if (client != null && client.getInputProtocol() != null) { client.getInputProtocol().getTransport().close(); client.getOutputProtocol().getTransport().close(); } } /** * 线程中断 */ public void interrupt() { this.isRunning=false; this.isOld=true; close(); super.interrupt(); } /** * 判断是否在运行状态 */ public boolean isRunning(){ return isRunning; }
根据上面的分析优化,android端即时消息收取,剩下的就是调整唤醒闹铃周期,平衡消息即时性与耗电的问题。
android即时消息处理机制