在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端即時消息收取,剩下的就是調整喚醒鬧鈴週期,平衡消息即時性與耗電的問題。