Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> IM推送Android客戶端SDK之智能心跳

IM推送Android客戶端SDK之智能心跳

編輯:關於Android編程

1. 為什麼TCP連接需要心跳?

因為運營商有一個NAT超時:因為IP v4的IP量有限,運營商分配給手機終端的IP是運營商內網的IP,手機要連接Internet,就需要通過運營商的網關做一個網絡地址轉換(Network Address Translation,NAT)。簡單的說運營商的網關需要維護一個外網IP、端口到內網IP、端口的對應關系,以確保內網的手機可以跟Internet的服務器通訊,大部分移動無線網絡運營商都在鏈路一段時間沒有數據通訊時,會淘汰NAT表中的對應項,造成鏈路中斷。
所以我們需要間隔一定的時間發送一個數據包來保證當前的TCP連接保持有效,這就是所謂的心跳包

2. 什麼是智能心跳?

智能心跳實際上就是動態的探測到最大的NAT超時時間,然後選定合適的心跳間隔區間去發送心跳包,同時在網絡狀況發生變化的時候能夠動態的調整心跳間隔時間;如果心跳間隔不合適,例如心跳間隔過短,那麼可能導致頻繁的喚醒手機發送心跳包,增加耗電,心跳間隔過長,可能導致這條TCP連接已經無效但是無法及時的檢測到,只能等待下一個心跳包發送的時候才能感知到,所以會導致消息接收延遲,所以探測到一個合適的心跳間隔是非常重要的,把耗電和消息接收及時性綜合折中來取得一個最佳的體驗

3. 我們的二分法智能心跳策略

心跳變量

探測心跳:程序采用不確定的時間間隔去發送心跳,目的是為了得到最大NAT超時時間 穩定心跳:當探測心跳探測到了NAT超時時間那麼就會選定比這個時間點稍微小一點的時間來作為穩定心跳,以後就一直以這個穩定時間去發送心跳 minHeart:最小的心跳間隔 maxHeart:最大的心跳間隔 curMinHeart:初始值為minHeart,變換過程中的最小心跳 curMaxHeart:初始值為maxHeart,變換過程中的最大心跳 step:心跳探測步長 maxSuccessCount:穩定心跳成功次數的最大值,用來動態向上探測 maxFailedCount:心跳連續失敗次數最大值,用來向下探測 curHeart:當前正在使用的心跳間隔,默認270秒,這個值可以根據不同地區的心跳區間大數據采集統計然後再設置 timeout:心跳超時時間,我們當前設置為20秒,這個其實可以調整的更小,5秒~10秒,之所以設置為20秒是考慮到網絡很不好的情況下可能心跳返回的比較慢,所以間隔設的大一些 heartbeatStabledSuccessCount:穩定心跳連續成功的次數 heartbeatFailedCount:心跳連續失敗的次數 networkTag:網絡環境標識,對於數據網絡來說分為電信,聯通,移動;對於wifi來說是用wifi的名稱來區分的,因為每個運營商的網絡環境都可能有不同的NAT超時,所以在網絡環境變換的時候要重新調整心跳

流程圖

二分法智能心跳

上調curHeart(心跳成功的時候)

把當前的成功心跳區間保存到列表中 curMinHeart = heartbeat.curHeart; 如果當前心跳是穩定心跳,heartbeatStabledFailedCount = 0;heartbeatStabledSuccessCount++;如果當前心跳不是穩定心跳,curHeart = (curMinHeart + curMaxHeart) / 2,然後直接執行第6步 判斷heartbeatStabledSuccessCount是否大於maxSuccessCount,如果大於的話就上調maxSuccessCount的上限,可以乘以2,或者遞增固定值,這個可以自己決定,我們是maxSuccessCount默認為20,所以maxSuccessCount = maxSuccessCount + 20; 從成功心跳列表選擇比當前穩定心跳更大一級心跳,如果有就把這個作為新的穩定心跳,如果沒有:curMaxHeart = maxHeart;curHeart = (curMinHeart + curMaxHeart) / 2;然後再重新以curHeart開始向上探測心跳 判斷curMaxHeart - curMinHeart < 10是否滿足,如果滿足並且當前心跳還不是穩定心跳:curHeart = curMinHeart;把二分法比較小的那個值作為穩定心跳,然後探測結束,進入穩定心跳,這裡之所以這麼做是因為二分法的一個特點,二分法的一個臨界值就是curMaxHeart = curMinHeart,到最後curMinHeart和curMaxHeart很接近的時候其實(curMaxHeart+curMinHeart)== 2*curMinHeart ==2*curMaxHeart,所以會導致二分法計算出來的curHeart和curMinHeart,curMaxHeart相差就幾秒,這是沒什麼意義的,設置一個10秒的區間來讓心跳盡快進入穩定狀態

下調心跳(心跳失敗的時候)

heartbeatStabledSuccessCount=0; curMaxHeart = curHeart; 如果是穩定心跳失敗了,heartbeatStabledFailedCount++;並且判斷heartbeatStabledFailedCount>maxFailedCount,如果是則從成功心跳列表中選擇比當前心跳略小一級的心跳並把這個心跳作為新的穩定心跳,要是不存在略小一級的成功心跳,那麼curMinHeart = minHeart;curHeart = (curMinHeart + curMaxHeart) / 2; 如果是探測心跳失敗了,curHeart = (curMinHeart + curMaxHeart) / 2; 判斷curMaxHeart - curMinHeart < 10,如果滿足並且當前心跳不是穩定心跳:curMinHeart = minHeart;

代碼實現

public abstract class HeartbeatScheduler {

    protected int timeout = 20000;

    protected int minHeart = 60;

    protected int maxHeart = 300;

    protected int step = 30;

    protected volatile boolean started = false;

    protected volatile long heartbeatSuccessTime;

    protected volatile int currentHeartType;

    public static final String HEART_TYPE_TAG = "heart_type";

    public static final int UNKNOWN_HEART = 0, SHORT_HEART = 1, PROBE_HEART = 2, STABLE_HEART = 3, REDUNDANCY_HEART = 4;

    protected PendingIntent createPendingIntent(Context context, int requestCode, int heartType) {
        Intent intent = new Intent();
        intent.setPackage(context.getPackageName());
        intent.setAction(SyncAction.HEARTBEAT_REQUEST);
        intent.putExtra(HEART_TYPE_TAG, heartType);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        return pendingIntent;
    }

    protected void set(int minHeart, int maxHeart, int step) {
        this.minHeart = minHeart;
        this.maxHeart = maxHeart;
        this.step = step;
        SyncLogUtil.i("set minMax:" + minHeart + ",maxHeart:" + maxHeart + ",step:" + step);
    }

    protected boolean isStarted() {
        return started;
    }

    protected abstract boolean isStabled();

    protected void setCurrentHeartType(int currentHeartType) {
        this.currentHeartType = currentHeartType;
        SyncLogUtil.i("set current heart type:" + currentHeartType);
    }

    protected int getTimeout() {
        return timeout;
    }

    protected void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    protected long getHeartbeatSuccessTime() {
        return heartbeatSuccessTime;
    }

    protected void setHeartbeatSuccessTime(long heartbeatSuccessTime) {
        this.heartbeatSuccessTime = heartbeatSuccessTime;
    }

    protected abstract void start(Context context);

    protected abstract void stop(Context context);

    protected abstract void clear(Context context);

    protected abstract void adjustHeart(Context context, boolean success);

    protected abstract void startNextHeartbeat(Context context, int heartType);

    protected abstract void resetScheduledHeart(Context context);

    protected abstract void receiveHeartbeatFailed(Context context);

    protected abstract void receiveHeartbeatSuccess(Context context);

    protected abstract int getCurHeart();
}
public class WatchHearbeatScheduler extends HeartbeatScheduler {

    private class Heartbeat {

        AtomicInteger heartbeatStabledSuccessCount = new AtomicInteger(0); // 心跳連續成功次數

        AtomicInteger heartbeatFailedCount = new AtomicInteger(0); // 心跳連續失敗次數

        int successHeart;

        int failedHeart;

        int curHeart = 270;

        AtomicBoolean stabled = new AtomicBoolean(false);

    }

    private int curMaxHeart = maxHeart;

    private int curMinHeart = minHeart;

    private int maxFailedCount = 5;

    private int maxSuccessCount = 20;

    private volatile String networkTag;

    private int requestCode = 700;

    private Map heartbeatMap = new HashMap<>();

    private List successHeartList = new ArrayList<>();

    protected WatchHearbeatScheduler() {

    }

    @Override
    protected void start(Context context) {
        started = true;
        networkTag = NetUtil.getNetworkTag(context);
        alarm(context);
        SyncLogUtil.i("start heartbeat,networkTag:" + networkTag);
    }

    @Override
    protected void stop(Context context) {
        heartbeatSuccessTime = 0;
        started = false;
        networkTag = null;
        currentHeartType = UNKNOWN_HEART;
        for (Map.Entry entry : heartbeatMap.entrySet()) {
            Heartbeat heartbeat = entry.getValue();
            heartbeat.heartbeatStabledSuccessCount.set(0);
            heartbeat.heartbeatFailedCount.set(0);
        }
        cancel(context);
        SyncLogUtil.d("stop heartbeat...");
    }

    @Override
    protected void setCurrentHeartType(int currentHeartType) {
        this.currentHeartType = currentHeartType;
    }

    @Override
    protected void set(int minHeart, int maxHeart, int step) {
        super.set(minHeart, maxHeart, step);
        curMaxHeart = maxHeart;
        curMinHeart = minHeart;
    }

    @Override
    protected boolean isStabled() {
        Heartbeat heartbeat = getHeartbeat();
        return heartbeat.stabled.get();
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    public void alarm(Context context) {
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        Heartbeat heartbeat = getHeartbeat();
        boolean stabled = heartbeat.stabled.get();
        int heart;
        if (stabled) {
            heart = heartbeat.curHeart - 10;
            if (heart < minHeart) {
                heart = minHeart;
            }
            heart = heart * 1000;
        } else {
            heart = heartbeat.curHeart * 1000;
        }
        int heartType = stabled ? STABLE_HEART : PROBE_HEART;
        PendingIntent pendingIntent = createPendingIntent(context, requestCode, heartType);
        int sdk = Build.VERSION.SDK_INT;
        if (sdk >= Build.VERSION_CODES.KITKAT) {
            alarmManager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + heart, pendingIntent);
        } else {
            alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + heart, pendingIntent);
        }
        SyncLogUtil.i("start heartbeat,curHeart [" + heartbeat.curHeart + "],heart [" + heart + "],requestCode:" + requestCode + ",stabled:" + stabled);
    }

    private void cancel(Context context) {
        Heartbeat heartbeat = getHeartbeat();
        int heartType = heartbeat.stabled.get() ? STABLE_HEART : PROBE_HEART;
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        PendingIntent pendingIntent = createPendingIntent(context, requestCode, heartType);
        alarmManager.cancel(pendingIntent);
        SyncLogUtil.d("cancel heartbeat,requestCode:" + requestCode);
    }

    @Override
    public void startNextHeartbeat(Context context, int heartType) {
        alarm(context);
    }

    @Override
    public void resetScheduledHeart(Context context) {
        alarm(context);
    }

    private void addSuccessHeart(Integer successHeart) {
        if (!successHeartList.contains(successHeart)) {
            if (successHeartList.size() > 10) {
                successHeartList.remove(0);
            }
            successHeartList.add(successHeart);
            SyncLogUtil.i("add successHeart:" + successHeart);
        }
        SyncLogUtil.i("successHeartList:" + successHeartList);
    }

    private void removeSuccessHeart(Integer successHeart) {
        successHeartList.remove(Integer.valueOf(successHeart));
        SyncLogUtil.i("successHeartList:" + successHeartList);
    }

    @Override
    protected void adjustHeart(Context context, boolean success) {
        if (currentHeartType == REDUNDANCY_HEART) {
            SyncLogUtil.d("redundancy heart,do not adjustHeart...");
            return;
        }

        Heartbeat heartbeat = getHeartbeat();
        if (success) {
            onSuccess(heartbeat);
        } else {
            onFailed(heartbeat);
        }
        SyncLogUtil.i("after success is [" + success +  "] adjusted,heartbeat.curHeart:" + heartbeat.curHeart + ",networkTag:" + networkTag);
    }

    private void onSuccess(Heartbeat heartbeat) {
        heartbeat.successHeart = heartbeat.curHeart;
        curMinHeart = heartbeat.curHeart;
        addSuccessHeart(heartbeat.successHeart);
        heartbeat.heartbeatFailedCount.set(0);
        if (heartbeat.stabled.get()) {
            int count = heartbeat.heartbeatStabledSuccessCount.incrementAndGet();
            SyncLogUtil.i("heartbeatStabledSuccessCount:" + heartbeat.heartbeatStabledSuccessCount.get());
            if (count >= maxSuccessCount) {
                maxSuccessCount += 20;
                SyncLogUtil.i("maxSuccessCount:" + maxSuccessCount);
                Integer successHeart = selectMinSuccessHeart(heartbeat.curHeart);
                if (successHeart != null) {
                    heartbeat.curHeart = successHeart;
                } else {
                    heartbeat.stabled.set(false);
                    curMaxHeart = maxHeart;
                    heartbeat.curHeart = (curMinHeart + curMaxHeart) / 2;
                    SyncLogUtil.i("curHeart = (" + curMinHeart + " + " + curMaxHeart + ") / 2 = " + heartbeat.curHeart);
                }
            }
        } else {
            heartbeat.curHeart = (curMinHeart + curMaxHeart) / 2;
            SyncLogUtil.i("curHeart = (" + curMinHeart + " + " + curMaxHeart + ") / 2 = " + heartbeat.curHeart);
        }
        if (heartbeat.curHeart >= maxHeart) {
            heartbeat.curHeart = maxHeart;
            heartbeat.stabled.set(true);
            SyncLogUtil.i("探測達到最大心跳adjust stabled:" + heartbeat.stabled.get());
        } else if (curMaxHeart - curMinHeart < 10) {
            if (!heartbeat.stabled.get()) {
                heartbeat.curHeart = curMinHeart;
            }
            heartbeat.stabled.set(true);
            SyncLogUtil.i("二分法探測盡頭adjust stabled:" + heartbeat.stabled.get());
        }
        SyncLogUtil.i("curHeart:" + heartbeat.curHeart + ",curMinHeart:" + curMinHeart + ",curMaxHeart:" + curMaxHeart);
    }

    private void onFailed(Heartbeat heartbeat) {
        removeSuccessHeart(heartbeat.curHeart);
        heartbeat.failedHeart = heartbeat.curHeart;
        heartbeat.heartbeatStabledSuccessCount.set(0);
        curMaxHeart = heartbeat.curHeart;
        int count = heartbeat.heartbeatFailedCount.incrementAndGet();
        SyncLogUtil.i("heartbeatFailedCount:" + count);
        if (maxSuccessCount > 20) {
            maxSuccessCount -= 20;
        }
        if (heartbeat.stabled.get()) {
            if (count > maxFailedCount) {
                Integer successHeart = selectMaxSuccessHeart(heartbeat.curHeart);
                if (successHeart != null) {
                    heartbeat.curHeart = successHeart;
                } else {
                    heartbeat.stabled.set(false);
                    curMinHeart = minHeart;
                    heartbeat.curHeart = (curMinHeart + curMaxHeart) / 2;
                    SyncLogUtil.i("curHeart = (" + curMaxHeart + " + " + curMinHeart + ") / 2 = " + heartbeat.curHeart);
                }
            } else {
                SyncLogUtil.i("continue retry heartbeat.curHeart:" + heartbeat.curHeart + ",stabled:" + heartbeat.stabled.get());
            }
        } else {
            if (count > maxFailedCount) {
                heartbeat.curHeart = (curMinHeart + curMaxHeart) / 2;
                SyncLogUtil.i("curHeart = (" + curMaxHeart + " + " + curMinHeart + ") / 2 = " + heartbeat.curHeart);
            } else {
                SyncLogUtil.i("continue retry heartbeat.curHeart:" + heartbeat.curHeart + ",stabled:" + heartbeat.stabled.get());
            }
        }
        if (curMaxHeart - curMinHeart < 10) {
            if (!heartbeat.stabled.get()) {
                curMinHeart = minHeart;
            }
            SyncLogUtil.i("二分法探測達到瓶頸" + ",curHeart:" + heartbeat.curHeart);
            SyncLogUtil.i("curMinHeart:" + curMinHeart + ",curMaxHeart:" + curMaxHeart);
        }
        SyncLogUtil.i("curHeart:" + heartbeat.curHeart + ",curMinHeart:" + curMinHeart + ",curMaxHeart:" + curMaxHeart);
    }

    private Integer selectMaxSuccessHeart(int curHeart) {
        Collections.sort(successHeartList, new Comparator() {
            @Override
            public int compare(Integer lhs, Integer rhs) {
                return rhs.compareTo(lhs);
            }
        });
        SyncLogUtil.i("successHeartList:" + successHeartList);
        for (Integer heart : successHeartList) {
            if (curHeart >= heart) {
                continue;
            } else {
                return heart;
            }
        }
        return null;
    }

    private Integer selectMinSuccessHeart(int curHeart) {
        Collections.sort(successHeartList, new Comparator() {
            @Override
            public int compare(Integer lhs, Integer rhs) {
                return lhs.compareTo(rhs);
            }
        });
        SyncLogUtil.i("successHeartList:" + successHeartList);
        for (Integer heart : successHeartList) {
            if (curHeart >= heart) {
                continue;
            } else {
                return heart;
            }
        }
        return null;
    }

    private Heartbeat getHeartbeat() {
        Heartbeat heartbeat = heartbeatMap.get(networkTag);
        if (heartbeat == null) {
            heartbeat = new Heartbeat();
            heartbeatMap.put(networkTag, heartbeat);
        }
        return heartbeat;
    }

    @Override
    protected void receiveHeartbeatFailed(Context context) {
        adjustHeart(context, false);
    }

    @Override
    protected void receiveHeartbeatSuccess(Context context) {
        adjustHeart(context, true);
        alarm(context);
    }

    @Override
    protected void clear(Context context) {
        stop(context);
        heartbeatMap.clear();
        successHeartList.clear();
        curMinHeart = minHeart;
        curMaxHeart = maxHeart;
        SyncLogUtil.d("clear heartbeat...");
    }

    @Override
    protected int getCurHeart() {
        Heartbeat heartbeat = getHeartbeat();
        return heartbeat.curHeart;
    }
}

4. 最終探測效果

以270秒作為curHeart開始探測,minHeart為60秒,maxHeart為300秒,在我們公司的wifi或者數據網絡環境下:270,285,292就能夠達到穩定心跳,最終穩定心跳會比292小10秒。也就是282秒作為穩定心跳,這裡面大概在14分鐘之內alarm了三次,如果把maxHeart上調的話探測到穩定心跳的時間會變長,不過平均alarm次數會降低,因為心跳周期在不斷變長 當達到穩定心跳後,在穩定心跳成功發送20次後會再次嘗試上調心跳,如果由於網絡環境不穩定導致當前的心跳可能失敗次數超過了5次,那麼就會下調心跳,總之做到一個原則,嚴格控制下調條件,能不下調就盡量不下調

5. 和微信智能心跳的對比

更加省電:微信智能心跳是按照從最小還是逐漸遞增的去探測的,所以在網絡環境不好的條件下前期可能一直探測不上來,心跳周期一直維持在一個較小的范圍,導致頻繁的alarm,耗電,微信智能心跳探測過程:60秒短心跳,連續發3次後開始探測,90,120,150,180,210,240,270,這個過程中一共耗費24分鐘,alarm了10次,在前14分鐘之內alarm了8次,而二分法智能心跳前14分鐘才喚醒3次 網絡環境差的情況下不會頻繁的喚醒:當網絡環境很不好的情況下,心跳可能會經常失敗,微信智能心跳由於是從下往上上調心跳,可能一直維持在一個間隔周期較小的心跳,會頻繁alarm,二分法是從上往下下調心跳,因此心跳周期是逐漸縮小,一開始不會頻繁的alarm,比較省電 探測周期短:微信智能心跳是逐漸的通過累加探測步長來上調心跳,上調的趨勢比較穩定,但是如果step設置的比較小,那麼會導致上調緩慢,探測到穩定心跳所需要的時間比較長(24分鐘);二分法智能心跳的心跳調整波動比較大,成功了就上調一半,失敗了就下調一半,所以探測到穩定心跳的時間會比較短(14分鐘),但是其實這個都是相對的,如果NAT超時時間為2分鐘,那麼微信智能心跳一下子就能探測到了,而二分法智能心跳要調整好多次,反正是看NAT超時時間距離最初開始探測的curHeat比較接近,所以curHeart可以通過大數據搜集分析,針對各個地區給出不同的curHeart 探測期間不夠穩定:微信智能心跳的探測過程很穩定,基本不會導致心跳失敗,因為它是從最小的開始探測;二分法智能心跳就不一樣了,以為curHeart的調整波動比較大,一開始探測一下子上調或者下調一半很容易就超出NAT超時時間,在探測前期會有比較頻繁的失敗心跳;當然,這個也是相對的,最終都要取決與curHeart的初始值,minHeart,maxHeart,如果這些值設置的合適,那麼二分法智能心跳將會很快的探測到穩定心跳

6. Android機子上存在的問題

alarm的對齊喚醒:國內的手機廠商例如華為,魅族,小米都是自定制的android系統,對於AlarmManager都有對齊喚醒策略,因此會導致心跳alarm的時間不准確,例如設置了270秒alarm一次,但是在這些手機上可能要推遲到300秒才能喚醒,那麼問題來了,如果NAT超時時間是2分鐘,而這些手機的alarm最小間隔是5分鐘,那就坑了,永遠無法探測到最佳心跳,你設置120秒的alarm,手機系統也給你延遲到5分鐘才執行alarm,不過這種情況只有在手機休眠的時候才會對齊喚醒,在手機不休眠的時候,我側過,alarm計時還是准確的
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved