在Android中實現推送方式的底層原理與推送的知識及相關解決方案

jopen 9年前發布 | 134K 次閱讀 Android Android開發 移動開發

最近一個月一直在考慮實現一種讓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中的實現方式。

來自:http://my.oschina.net/kymjs/blog/367325

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!