上一篇文章中我們講解了Android app實現長連接的幾種方式,各自的優缺點以及具體的實現,一般而言使用第三方的推送服務已經可以滿足了基本的業務需求,當然了若是對技術有追求的可以通過NIO或者是MINA實現自身的長連接服務,但是自己實現的長連接服務一來比較復雜耗時比較多,而且可能過程中有許多坑要填,一般而言推薦使用第三方的推送服務,穩定簡單,具體管理長連接部分的模塊可參考:[Android產品研發(十二)–>App長連接實現](http://blog.csdn.net/qq_23547831/article/details/51671005)。
而本文將講解app端的輪詢請求服務,一般而言我們經常將輪詢操作用于請求服務器。比如某一個頁面我們有定時任務需要時時的從服務器獲取更新信息并顯示,比如當長連接斷掉之后我們可能需要啟動輪詢請求作為長連接的補充等,所以這時候就用到了輪詢服務。
**什么是輪詢請求?**
在說明我們輪詢請求之前,這里先說明一下什么叫輪詢請求,我的理解就是App端每隔一定的時間重復請求的操作就叫做輪詢請求,比如:App端每隔一段時間上報一次定位信息,App端每隔一段時間拉去一次用戶狀態等,這些應該都是輪詢請求,那么前一篇我們講了App端的長連接,為什么我們有了長連接之后還需要輪詢操作呢?
這是因為我們的長連接并不是穩定的可靠的,而我們執行輪詢操作的時候一般都是要穩定的網絡請求,而且輪詢操作一般都是有生命周期的,即在一定的生命周期內執行輪詢操作,而我們的長連接一般都是整個進程生命周期的,所以從這方面講也不太適合。
**輪詢請求實踐**
**與長連接相關的輪詢請求**
1. 上一篇我們在講解長連接的時候說過長連接有可能會斷,而這時候在長連接斷的時候我們就需要啟動一個輪詢服務,它作為長連接的補充。
~~~
/**
* 啟動輪詢服務
*/
public void startLoopService() {
// 啟動輪詢服務
// 暫時不考慮加入網絡情況的判斷...
if (!LoopService.isServiceRuning) {
// 用戶是登錄態,啟動輪詢服務
if (UserConfig.isPassLogined()) {
// 判斷當前長連接的狀態,若長連接已連接,則不再開啟輪詢服務
if (MinaLongConnectManager.session != null && MinaLongConnectManager.session.isConnected()) {
LoopService.quitLoopService(context);
return;
}
LoopService.startLoopService(context);
} else {
LoopService.quitLoopService(context);
}
}
}
~~~
這里就是我們執行輪詢服務的操作代碼,其作用就是啟動了一個輪詢service(即輪詢服務),然后在輪詢服務中執行具體的輪詢請求,既然這樣我們就具體看一下這個service的代碼邏輯。
- 與長連接相關的輪詢服務請求
~~~
/**
* 長連接異常時啟動服務,長連接恢復時關閉服務
*/
public class LoopService extends Service {
public static final String ACTION = "com.youyou.uuelectric.renter.Service.LoopService";
/**
* 客戶端執行輪詢的時間間隔,該值由StartQueryInterface接口返回,默認設置為30s
*/
public static int LOOP_INTERVAL_SECS = 30;
/**
* 輪詢時間間隔(MLOOP_INTERVAL_SECS 這個時間間隔變量有服務器下發,此時輪詢服務的場景與邏輯與定義時發生變化,涉及到IOS端,因此采用自己定義的常量在客戶端寫死時間間隔)
*/
public static int MLOOP_INTERVAL_SECS = 30;
/**
* 當前服務是否正在執行
*/
public static boolean isServiceRuning = false;
/**
* 定時任務工具類
*/
public static Timer timer = new Timer();
private static Context context;
public LoopService() {
isServiceRuning = false;
}
//-------------------------------使用鬧鐘執行輪詢服務------------------------------------
/**
* 啟動輪詢服務
*/
public static void startLoopService(Context context) {
if (context == null)
return;
quitLoopService(context);
L.i("開啟輪詢服務,輪詢間隔:" + MLOOP_INTERVAL_SECS + "s");
AlarmManager manager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context.getApplicationContext(), LoopService.class);
intent.setAction(LoopService.ACTION);
PendingIntent pendingIntent = PendingIntent.getService(context.getApplicationContext(), 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// long triggerAtTime = SystemClock.elapsedRealtime() + 1000;
/**
* 鬧鐘的第一次執行時間,以毫秒為單位,可以自定義時間,不過一般使用當前時間。需要注意的是,本屬性與第一個屬性(type)密切相關,
* 如果第一個參數對應的鬧鐘使用的是相對時間(ELAPSED_REALTIME和ELAPSED_REALTIME_WAKEUP),那么本屬性就得使用相對時間(相對于系統啟動時間來說),
* 比如當前時間就表示為:SystemClock.elapsedRealtime();
* 如果第一個參數對應的鬧鐘使用的是絕對時間(RTC、RTC_WAKEUP、POWER_OFF_WAKEUP),那么本屬性就得使用絕對時間,
* 比如當前時間就表示為:System.currentTimeMillis()。
*/
manager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), MLOOP_INTERVAL_SECS * 1000, pendingIntent);
}
/**
* 停止輪詢服務
*/
public static void quitLoopService(Context context) {
if (context == null)
return;
L.i("關閉輪詢鬧鐘服務...");
AlarmManager manager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context.getApplicationContext(), LoopService.class);
intent.setAction(LoopService.ACTION);
PendingIntent pendingIntent = PendingIntent.getService(context.getApplicationContext(), 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
manager.cancel(pendingIntent);
// 關閉輪詢服務
L.i("關閉輪詢服務...");
context.stopService(intent);
}
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
L.i("開始執行輪詢服務... \n 判斷當前用戶是否已登錄...");
// 若當前網絡不正常或者是用戶未登陸,則不再跳轉
if (UserConfig.isPassLogined()) {
// 判斷當前長連接狀態,若長連接正常,則關閉輪詢服務
L.i("當前用戶已登錄... \n 判斷長連接是否已經連接...");
if (MinaLongConnectManager.session != null && MinaLongConnectManager.session.isConnected()) {
L.i("長連接已恢復連接,退出輪詢服務...");
quitLoopService(context);
} else {
if (isServiceRuning) {
return START_STICKY;
}
// 啟動輪詢拉取消息
startLoop();
}
} else {
L.i("用戶已退出登錄,關閉輪詢服務...");
quitLoopService(context);
}
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
L.i("輪詢服務退出,執行onDestory()方法,inServiceRuning賦值false");
isServiceRuning = false;
timer.cancel();
timer = new Timer();
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
/**
* 啟動輪詢拉去消息
*/
private void startLoop() {
if (timer == null) {
timer = new Timer();
}
timer.schedule(new TimerTask() {
@Override
public void run() {
isServiceRuning = true;
L.i("長連接未恢復連接,執行輪詢操作... \n 輪詢服務中請求getInstance接口...");
LoopRequest.getInstance(context).sendLoopRequest();
}
}, 0, MLOOP_INTERVAL_SECS * 1000);
}
}
~~~
可以發現這里的service輪詢服務的代碼量還是比較多的,但是輪詢服務請求代碼注釋已經很詳細了,所以就不做過多的說明,需要說明的是其核心就是通過Timer對象每個一段時間執行一次網絡請求。具體的網絡請求代碼:
~~~
L.i("長連接未恢復連接,執行輪詢操作... \n 輪詢服務中請求getInstance接口...");
LoopRequest.getInstance(context).sendLoopRequest();
~~~
這里的輪詢服務請求核心邏輯:當長連接出現異常時,啟動輪詢服務,并通過Timer對象每隔一定時間拉取服務器狀態,當長連接恢復時,關閉輪詢服務。這就是我們與長連接有關的輪詢服務的代碼執行邏輯,看完這部分之后我們再看一下與頁面相關的輪詢請求的執行邏輯。
**與頁面相關的輪詢請求**
- 與頁面相關的輪詢請求
我們的App中當用戶停留在某一個頁面的時候我們可能需要定時的拉取用戶狀態,這時候也需要使用輪詢請求拉取服務器狀態,當用戶離開該頁面的時候關閉輪詢服務請求。
這里我們看一下我們產品當前行程頁面的輪詢操作,用于輪詢請求當前用戶的車輛里程,費用,用時等信息,具體可參考下圖:

其實在當前Fragment頁面有一個定時的拉去訂單信息的輪詢請求,下面我們具體看一下這個定時請求的執行邏輯:
~~~
/**
* TimerTask對象,主要用于定時拉去服務器信息
*/
public class Task extends TimerTask {
@Override
public void run() {
L.i("開始執行執行timer定時任務...");
handler.post(new Runnable() {
@Override
public void run() {
isFirstGetData = false;
getData(true);
}
});
}
}
~~~
而這里的getData方法就是拉去服務器狀態的方法,這里不做過多的解釋,當用戶退出這個頁面的時候需要清除這里的輪詢操作。所以在Fragment的onDesctoryView方法中執行了清除timerTask的操作。
~~~
@Override
public void onDestroyView() {
super.onDestroyView();
...
if (timer != null) {
timer.cancel();
timer = null;
}
if (timerTask != null) {
timerTask.cancel();
timerTask = null;
}
...
}
~~~
這樣當用戶打開這個頁面的時候初始化TimerTask對象,每個一分鐘請求一次服務器拉取訂單信息并更新UI,當用戶離開頁面的時候清除TimerTask對象,即取消輪詢請求操作。可以發現上面我們看到的與長連接和頁面相關的輪詢請求服務都是通過timer對象的定時任務實現的輪詢請求服務,下面我們看一下如何通過Handler對象實現輪詢請求服務。
**通過Handler對象實現輪詢請求**
- 下面我們來看一個通過Handler異步消息實現的輪詢請求服務。
~~~
/**
* 默認的時間間隔:1分鐘
*/
private static int DEFAULT_INTERVAL = 60 * 1000;
/**
* 異常情況下的輪詢時間間隔:5秒
*/
private static int ERROR_INTERVAL = 5 * 1000;
/**
* 當前輪詢執行的時間間隔
*/
private static int interval = DEFAULT_INTERVAL;
/**
* 輪詢Handler的消息類型
*/
private static int LOOP_WHAT = 10;
/**
* 是否是第一次拉取數據
*/
private boolean isFirstRequest = false;
/**
* 第一次請求數據是否成功
*/
private boolean isFirstRequestSuccess = false;
/**
* 開始執行輪詢,正常情況下,每隔1分鐘輪詢拉取一次最新數據
* 在onStart時開啟輪詢
*/
private void startLoop() {
L.i("頁面onStart,需要開啟輪詢");
loopRequestHandler.sendEmptyMessageDelayed(LOOP_WHAT, interval);
}
/**
* 關閉輪詢,在界面onStop時,停止輪詢操作
*/
private void stopLoop() {
L.i("頁面已onStop,需要停止輪詢");
loopRequestHandler.removeMessages(LOOP_WHAT);
}
/**
* 處理輪詢的Handler
*/
private Handler loopRequestHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 如果首次請求失敗,
if (!isFirstRequestSuccess) {
L.i("首次請求失敗,需要將輪詢時間設置為:" + ERROR_INTERVAL);
interval = ERROR_INTERVAL;
} else {
interval = DEFAULT_INTERVAL;
}
L.i("輪詢中-----當前輪詢間隔:" + interval);
loopRequestHandler.removeMessages(LOOP_WHAT);
// 首次請求為成功、或者定位未成功時執行重定位,并加載網點數據
if (!isFirstRequestSuccess || !Config.locationIsSuccess()) {
isClickLocationButton = false;
doLocationOption();
} else {
loadData();
}
System.gc();
loopRequestHandler.sendEmptyMessageDelayed(LOOP_WHAT, interval);
}
};
~~~
這里是通過Handler實現的輪詢操作,其核心原理就是在handler的handlerMessage方法中,接收到消息之后再次發送延遲消息,這里的延遲時間就是我們定義的輪詢間隔時間,這樣當我們下次接收到消息的時候又一次發送延遲消息,從而造成我們時時發送輪詢消息的情景。
以上就是我們實現輪詢操作的兩種方式:
* Timer對象實現輪詢操作
* Handler對象實現輪詢操作
上面我們分析了輪詢請求的不同使用場景,作用以及實現方式,當我們在具體的開發過程中需要定時的向服務器拉取消息的時候就可以考慮使用輪詢請求了。
**總結**:
* 輪詢操作一般都是通過定時請求服務器拉取信息并更新UI;
* 輪詢操作一般都有一定的生命周期,比如在某個頁面打開時啟動輪詢操作,在某個頁面關閉時取消輪詢操作;
* 輪詢操作的請求間隔需要根據具體的需求確定,間隔時間不宜過短,否則可能造成并發性問題;
* 產品開發過程中,某些需要試試更新服務器拉取信息并更新UI時,可以考慮使用輪詢操作實現;
* 可以通過Timer對象和Handler對象兩種方式實現輪詢請求操作;
另外對產品研發技術,技巧,實踐方面感興趣的同學可以參考我的:
[android產品研發(一)-->實用開發規范 ](http://blog.csdn.net/qq_23547831/article/details/51534013)
[android產品研發(二)-->啟動頁優化 ](http://blog.csdn.net/qq_23547831/article/details/51541277)
[android產品研發(三)-->基類Activity ](http://blog.csdn.net/qq_23547831/article/details/51546974)
[android產品研發(四)-->減小Apk大小](http://blog.csdn.net/qq_23547831/article/details/51559066)
[android產品研發(五)-->多渠道打包](http://blog.csdn.net/qq_23547831/article/details/51569261)
[Android產品研發(六)–>Apk混淆](http://blog.csdn.net/qq_23547831/article/details/51581491)
[android產品研發(七)-->Apk熱修復](http://blog.csdn.net/qq_23547831/article/details/51587927)
[Android產品研發(八)–>App數據統計](http://blog.csdn.net/qq_23547831/article/details/51612429)
[Android產品研發(九)–>App網絡傳輸協議](http://blog.csdn.net/qq_23547831/article/details/51655330)
[Android產品研發(十)–>不使用靜態變量保存數據](http://blog.csdn.net/qq_23547831/article/details/51685310)
[Android產品研發(十一)–>應用內跳轉scheme協議](http://blog.csdn.net/qq_23547831/article/details/51685310)
[Android產品研發(十二)–>App長連接實現](http://blog.csdn.net/qq_23547831/article/details/51719389)
- 前言
- (一)–>實用開發規范
- (二)-->啟動頁優化
- (三)-->基類Activity
- (四)-->減小Apk大小
- (五)-->多渠道打包
- (六)-->Apk混淆
- (七)-->Apk熱修復
- (八)-->App數據統計
- (九)-->App網絡數據解析
- (十)-->盡量不使用靜態變量保存數據
- (十一)-->應用內跳轉Scheme協議
- (十二)-->App長連接實現
- (十三)-->App輪詢操作
- (十四)-->App升級與更新
- (十五)-->內存對象序列化
- (十六)-->開發者選項
- (十七)-->Hybrid開發
- (十八)-->webview問題集錦
- (十九)-->Android studio中的單元測試
- (二十)-->代碼Review
- (二十一)-->Android中的UI優化
- (二十二)-->Android實用調試技巧
- (二十三)-->Android中保存靜態秘鑰實踐
- (二十四)-->內存泄露場景與檢測
- (二十五)-->MVC/MVVM/MVP簡單理解