自己動手做推送,動手做推
最近一個月一直在考慮實現一種讓Android開發人員一個人就能完成的推送功能庫。因為現有的推送功能,全部都需要伺服器端配合,不斷測試,即使使用第三方庫也需要很長一段時間的測試。這裡就是我最近研究的一個小小的成果:http://git.oschina.net/kymjs/KJPush
推送功能在Android應用開發中已經非常普遍了,本文就是來探討下Android中推送的底層原理與實現推送功能的一些解決方案。
1、什麼是推送?
當我們開發需要和伺服器互動的應用程式時,基本上都需要擷取伺服器端的資料,比如開源中國用戶端,在有人評論或回複你的時候,用戶端需要知道,並作出相應處理。要擷取伺服器上的資訊,有兩種方法:第一種是用戶端使用Pull(拉)的方式,就是隔一段時間就去伺服器上擷取一下資訊,看是否有更新的資訊出現。第二種就是伺服器使用Push(推送)的方式,當伺服器端有新資訊了,則把最新的資訊Push到用戶端上。這樣,用戶端就能自動的接收到訊息。
Push是服務端主動發訊息給用戶端,現在有很多第三方推送架構:例如百度推送、極光推送、個推等等,都是基於之前說的第二種方式也就是伺服器使用Push的方式。因為第一時間知道資料發生變化的是伺服器自己,所以Push的優勢是即時性高。但伺服器主動推送需要單獨開發一套能讓用戶端持久已連線的服務端程式。但有些情況下並不需要服務端主動推送,而是在一定的時間間隔內用戶端主動發起查詢,這種時候就應該使用Pull的方式去擷取。很多人認為Push方式沒有任何消耗,其實不然採用Push方式需要長時間維持一條用戶端與伺服器端通訊的socket長串連,依舊是很費流量與電量。如果輪詢策略配置的好,消耗的電與資料流量絕不比維持一個socket串連使用的多。譬如有這樣一個app,即時性要求不高,每天只要能擷取10次最新資料就能滿足要求了,這種情況顯然輪詢更適合一些,推送顯得太浪費,而且更耗電。
2、如何?輪詢請求
第一種方式是在一個Service中建立一個計時器,如下代碼是在網上找的一段類似實現(節選)
/** * 簡訊推送服務類,在後台長期運行,每個一段時間就向伺服器發送一次請求 * @author jerry */public class PushSmsService extends Service { @Override public void onCreate() { this.client = new AsyncHttpClient(); this.myThread = new MyThread(); this.myThread.start(); super.onCreate(); } private class MyThread extends Thread { @Override public void run() { String url = "你請求的網路地址"; while (flag) { // 每個10秒向伺服器發送一次請求 Thread.sleep(10000); // 採用get方式向伺服器發送請求 client.get(url, new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { try { JSONObject result = new JSONObject(new String( responseBody, "utf-8")); int state = result.getInt("state"); // 假設偶數為未讀訊息 if (state % 2 == 0) { String content = result.getString("content"); String date = result.getString("date"); String number = result.getString("number"); notification(content, number, date); } } catch (Exception e) { e.printStackTrace(); } }}
但是用Sleep,TimerTask,都會增大Service被系統回收的可能,更合適的方法是使用AlarmManager這個系統的計時器去管理。
實現方法如下,你可以在這裡看到完整的實現方式
private void startRequestAlarm() { cancelRequestAlarm(); // 從1秒後開始,每隔2分鐘執行getOperationIntent() // 注意,這個2分鐘只是正常情況下的2分鐘,實際情況可能不同系統的處理策略而被延長,比如坑爹的粗糧系統上可能被延長至5分鐘 mAlarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, KJPushConfig.PALPITATE_TIME, getOperationIntent()); } /** * 即使啟動PendingIntent的原進程結束了的話,PendingIntent本身仍然還存在,可在其他進程( * PendingIntent被遞交到的其他程式)中繼續使用. * 如果我在從系統中提取一個PendingIntent的,而系統中有一個和你描述的PendingIntent對等的PendingInent, * 那麼系統會直接返回和該PendingIntent其實是同一token的PendingIntent, * 而不是一個新的token和PendingIntent。然而你在從提取PendingIntent時,通過FLAG_CANCEL_CURRENT參數, * 讓這個老PendingIntent的先cancel()掉,這樣得到的pendingInten和其token的就是新的了。 */ private void cancelRequestAlarm() { mAlarmMgr.cancel(getOperationIntent()); } /** * 採用輪詢方式實現訊息推送<br> * 每次被調用都去執行一次{@link #PushReceiver}onReceive()方法 * * @return */ private PendingIntent getOperationIntent() { Intent intent = new Intent(this, PushReceiver.class); intent.setAction(KJPushConfig.ACTION_PULL_ALARM); PendingIntent operation = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); return operation; }
這樣就可以在最大程度上解決因為自己實現計時器造成的計時不準確或計時器被系統回收的問題。
但是僅僅這樣還沒辦法實現一個完善且穩定的輪詢推送庫,做推送最大的問題有三個:電量消耗,資料流量消耗,服務持久化。
3、電量消耗最佳化與資料流量消耗最佳化:
這兩個問題其實可以合并成一個問題,因為請求伺服器其實也是一個費電的事情。與維持一個長串連類似,要實現推送功能,不管是維持一個長串連或者是定時請求伺服器都需要耗費網路資料流量,而只不過長串連是一個細水長流不斷耗費,而輪詢是一次一大斷資料的耗費。這樣就需要一種可行的策略去配置,讓輪詢按照我們想要的方式去執行。目前我採用的思路是當手機處於GPRS模式時降低輪詢的頻率,每5分鐘請求一次伺服器,當手機處於WiFi模式時每2分鐘請求一次伺服器,同時設定如果熄滅螢幕則停止推送請求,當螢幕熄滅20秒後殺死推送進程,這樣不僅不需要考慮維護一個進程的消耗同時也節省了資料流量的使用。
4、服務持久化
相信這是一個很多人都遇到的問題,網上也有很多類似的問題,像QQ這種應用做的就非常好,不管使用第三方手機小幫手或者使用系統停止一個應用(不是設定裡面的那種停止,是長按Home鍵的那種),後台Service都不會被回收。很可惜,我目前只能做到保證一個Service不被第三方手機小幫手回收,可以防止部分手機長按Home鍵停止,但是例如粗糧的MIUI系統,依舊會殺死我的Service且無法恢複。目前為止我依舊沒有找到一個公開的完美解決辦法,如果你知道如何解決,請不吝指教。下面我就簡單講講如何最大程度的維護一個Service。
以前在做音樂播放器的時候,相信很多人都遇到了,在應用開啟過多的時候,後台播放音樂的Service獨立進程會被系統殺死。
在Android的ActivityManager中有一個內部類RunningAppProcessInfo,用來記錄當前系統中進程的狀態,如下是其中的一些值:
/** * Constant for {@link #importance}: this is a persistent process. * Only used when reporting to process observers. * @hide */ public static final int IMPORTANCE_PERSISTENT = 50; /** * Constant for {@link #importance}: this process is running the * foreground UI. */ public static final int IMPORTANCE_FOREGROUND = 100; /** * Constant for {@link #importance}: this process is running something * that is actively visible to the user, though not in the immediate * foreground. */ public static final int IMPORTANCE_VISIBLE = 200; /** * Constant for {@link #importance}: this process is running something * that is considered to be actively perceptible to the user. An * example would be an application performing background music playback. */ public static final int IMPORTANCE_PERCEPTIBLE = 130; /** * Constant for {@link #importance}: this process is running an * application that can not save its state, and thus can't be killed * while in the background. * @hide */ public static final int IMPORTANCE_CANT_SAVE_STATE = 170; /** * Constant for {@link #importance}: this process is contains services * that should remain running. */ public static final int IMPORTANCE_SERVICE = 300; /** * Constant for {@link #importance}: this process process contains * background code that is expendable. */ public static final int IMPORTANCE_BACKGROUND = 400; /** * Constant for {@link #importance}: this process is empty of any * actively running code. */ public static final int IMPORTANCE_EMPTY = 500;
一般數值大於RunningAppProcessInfo.IMPORTANCE_SERVICE的進程都長時間沒用或者空進程了
一般數值大於RunningAppProcessInfo.IMPORTANCE_VISIBLE的進程都是非可見進程,也就是在後台運行著
第三方清理軟體清理的一般是大於IMPORTANCE_VISIBLE的值,所以要想不被殺死就需要將自己的進程降低到IMPORTANCE_VISIBLE以下,也就是可見進程的程度。在每一個Service中有一個方法叫startForeground,也就是以可見進程的模式啟動,這裡是在SDK源碼中的實現與注釋,可以看到,它會在通知欄持續顯示一個通知,但只需要將id傳為0即可避免通知的顯示。當然要取消這種可見進程等級的設定只需要調用stopForgeround即可。
/** * Make this service run in the foreground, supplying the ongoing * notification to be shown to the user while in this state. * By default services are background, meaning that if the system needs to * kill them to reclaim more memory (such as to display a large page in a * web browser), they can be killed without too much harm. You can set this * flag if killing your service would be disruptive to the user, such as * if your service is performing background music playback, so the user * would notice if their music stopped playing. */ public final void startForeground(int id, Notification notification) { try { mActivityManager.setServiceForeground( new ComponentName(this, mClassName), mToken, id, notification, true); } catch (RemoteException ex) { } }
這裡由於篇幅有限就講這麼多了,希望詳細瞭解進程優先權提升的可以看看ActivityManager源碼中的定義以及KJPush中的實現方式。