android即時消息處理機制

     在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, "NotifyService@@===>啓動空請求");
             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, "ApNotifyThread@@===>空請求開始處理 threadID="+Thread.currentThread().getId());
            this.isRunning=true;
            if(this.isDoShortRequest){
                if(shortEmptyRequest()&&this.isRunning)
                    longEmptyRequest(); //再開啓長空請求
            }else{
                longEmptyRequest();
            }
            ImLog.d(TAG, "ApNotifyThread@@===>"+(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, "longEmptyRequest@@===>無可用網絡");
                        break;
                    }
                    try {
                        if(!handleMessage())
                            break;
                     } catch (TException e) {
                         if(!this.isRunning)
                             break;
                         ImLog.d(TAG, "longEmptyRequest@@===>發請求異常:"+ e.getMessage());
                         if(exceptionHandler(e)){
                             throw new IMException("連接失敗次數過多",MessageCode.IM_EXCEPTION_CONNECT);
                         }
                         continue;
                     }
                }
                ImLog.d(TAG, "longEmptyRequest@@===>"+(this.isOld?"上一個":"")+"空請求正常退出");
            } catch (Exception e) {
                ImLog.d(TAG, "longEmptyRequest@@===>"+(this.isOld?"上一個":"")+"空請求異常退出"+e.getMessage());
                if (exceptionHandler(e)) {
                    // 調用重連
                    ImLog.d(TAG, "longEmptyRequest@@===>調用重連");
                    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, "shortEmptyRequest@@===>無可用網絡");
                    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, "shortEmptyRequest@@===>notifyID:"+NotifyID.notifyID.get());
                messageNum=  apclient.getNotifyMsgSize(sessionID, ticket, NotifyID.notifyID.get());
                NotifyError.notifyErrorNum.set(0);
                ImLog.d(TAG, "shortEmptyRequest@@===>獲取消息條數:"+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, "shortEmptyRequest@@===>無可用網絡");
                            break;
                        }
                        if(!handleMessage())
                            break;
                        receiveMessageNum++;
                        if(receiveMessageNum==messageNum) //短連接接收完後退出
                            break;
                    }
                }
                ImLog.d(TAG, "shortEmptyRequest@@===>"+(this.isOld?"上一個":"")+"空請求正常退出");
            }catch(Exception e){
                ImLog.d(TAG, "shortEmptyRequest@@===>"+(this.isOld?"上一個":"")+"空請求異常退出"+e.getMessage());
                if(exceptionHandler(e)){
                    isDoLongRequest=false;
                    //調用重連
                    ImLog.d(TAG, "shortEmptyRequest@@===>調用重連");
                    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, "handleMessage@@===>一次空請求週期開始 ");
            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, "handleMessage@@===>進入接收數據處理");
                    while (processor.process(protocol, 
                            protocol) == true) {
                        isStop = true;
                        break;
                    }
                    ImLog.d(TAG, "handleMessage@@===>結束接收數據處理");
                } catch (TException e) {
                    ImLog.d(TAG, "handleMessage@@===>接收數據處理異常");
                    isStop = true;
                }
            }
            ImLog.d(TAG, "handleMessage@@===>一次空請求週期結束");
            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端即時消息收取,剩下的就是調整喚醒鬧鈴週期,平衡消息即時性與耗電的問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章