為了提高用戶(hù)體驗(yàn)鞭光,現(xiàn)在大多數(shù)的應(yīng)用都會(huì)增加推送功能吏廉,目前主流的第三方推送有 個(gè)推、mi push惰许、百度席覆、Jpush、極光等汹买,但是推送的到達(dá)率卻是不盡人意的佩伤,拿個(gè)推而言,服務(wù)器這邊統(tǒng)計(jì)的結(jié)果是到達(dá)率僅有90%(僅做參考)晦毙。當(dāng)然了還有官方的推送Google Cloud Messaging生巡,可惜在國(guó)內(nèi)然并卵,暫不做討論见妒。
推送到達(dá)率問(wèn)題的解決是刻不容緩的孤荣,因?yàn)樵谀壳盎ヂ?lián)網(wǎng)大用戶(hù)量的場(chǎng)景下,10%的用戶(hù)數(shù)還是相當(dāng)大的。
原因
我們知道盐股,推送的技術(shù)原理主要是保持網(wǎng)絡(luò)的長(zhǎng)連接钱豁,在TCP長(zhǎng)連接建立成功的基礎(chǔ)上,推送不能如期到達(dá)的原因主要和網(wǎng)絡(luò)狀況有關(guān)疯汁,比如網(wǎng)絡(luò)慢牲尺、丟包等等,這個(gè)是所有網(wǎng)絡(luò)訪(fǎng)問(wèn)遇到的問(wèn)題幌蚊,不是導(dǎo)致推送到達(dá)率如此低的主要原因谤碳。
那么,其最主要原因是什么呢霹肝?顯然是TCP長(zhǎng)連接持續(xù)保持這個(gè)前提未能得到保證估蹄,也就是:
推送時(shí),移動(dòng)端未在線(xiàn)
解決方案
現(xiàn)在我們找到了其原因所在沫换,那么要解決這個(gè)問(wèn)題臭蚁,就要從兩方面入手:
- “不擇手段”的保證移動(dòng)端在線(xiàn),保證TCP長(zhǎng)連接持續(xù)建立
- 緩存推送消息讯赏,用戶(hù)上線(xiàn)后重新發(fā)送
保證移動(dòng)端在線(xiàn)
其實(shí)也就是我們常說(shuō)的進(jìn)程笨宥遥活,可以創(chuàng)建一個(gè)幽靈進(jìn)程進(jìn)行笔妫活操作系枪,也可以直接用應(yīng)用主進(jìn)程進(jìn)行保活磕谅,用這個(gè)進(jìn)程中建立TCP連接,保證其存活的最大時(shí)長(zhǎng)衬浑。方案主要以下幾種:
- 利用系統(tǒng)廣播拉起應(yīng)用放刨,包括系統(tǒng)廣播和同系列應(yīng)用廣播
- 啟動(dòng)前臺(tái)service工秩,由于我們不想讓用戶(hù)感知到,所以應(yīng)利用系統(tǒng)漏洞取消通知欄Notification的顯示
注意:
保證移動(dòng)端在線(xiàn)確實(shí)能有效的提高推送的到達(dá)率进统,但是需要注意頻繁的喚醒應(yīng)用會(huì)導(dǎo)致應(yīng)用耗電量的增加,所以要把握一定的度螟碎。
廣播喚醒
利用系統(tǒng)廣播
監(jiān)聽(tīng)系統(tǒng)事件廣播來(lái)喚醒應(yīng)用,常用的廣播有:
- 開(kāi)機(jī)掉分,
ACTION_BOOT_COMPLETED
- 亮屏倍谜,
ACTION_SCREEN_ON
- 滅屏迈螟,
ACTION_SCREEN_OFF
- 插拔有線(xiàn)耳機(jī),
ACTION_HEADSET_PLUG
- 電量充足答毫,
ACTION_BATTERY_OK
注意:
- 部分機(jī)型可能對(duì)開(kāi)機(jī)廣播做了限制季春,所以可能收不到開(kāi)機(jī)廣播
-
ACTION_SCREEN_ON
、ACTION_SCREEN_OFF
载弄、ACTION_HEADSET_PLUG
廣播只能在代碼里注冊(cè),當(dāng)app完全退出后就收不到這個(gè)廣播了
不同的app進(jìn)程惫叛,用廣播相互喚醒
- 嵌入第三方SDK會(huì)喚醒相應(yīng)的app進(jìn)程逞刷,比如
- 微信的SDK會(huì)喚醒微信應(yīng)用,支付寶支付的SDK會(huì)喚醒支付寶SDK
- 個(gè)推SDK會(huì)喚醒其他嵌入個(gè)推SDK的應(yīng)用
- App會(huì)喚醒同公司的其他app夸浅,比如:支付寶、天貓警医、淘寶坯钦、UC等阿里系的應(yīng)用,打開(kāi)其中一款就有可能順便喚醒其他幾款應(yīng)用
前臺(tái)service
該方案是應(yīng)用范圍最最廣泛的一種手段吟温,主要是啟動(dòng)一個(gè)前臺(tái)service路星,并利用系統(tǒng)漏洞避免其在通知欄處顯示Notification诱桂。這樣既能保證進(jìn)程的優(yōu)先級(jí)高于普通后臺(tái)進(jìn)程,又將用戶(hù)感知降到最低挥等。
思路:
- API < 18時(shí),啟動(dòng)前臺(tái)Service時(shí)直接傳入
new Notification()
- API >= 18迁客,同時(shí)啟動(dòng)兩個(gè)id相同并傳入
new Notification()
的前臺(tái)Service,然后再將后啟動(dòng)的Service做stop處理
public class DaemonService extends Service {
private static final int DAEMON_SERVICE_ID = 123456789;
private static boolean mAlive = false;
@Override
public void onCreate() {
super.onCreate();
mAlive = true;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
// API < 18時(shí)粘室,直接傳入new Notification()
startForeground(DAEMON_SERVICE_ID, new Notification());
} else {
// API >= 18時(shí)卜范,啟動(dòng)兩個(gè)id相同的service,然后將后startForeground的service stopForeground/stop
startService(new Intent(this, DaemonInnerService.class));
startForeground(DAEMON_SERVICE_ID, new Notification());
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
mAlive = false;
}
public static boolean isAlive() {
return mAlive;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
/**
* 用于API >= 18時(shí)灰色苯蹙簦活Service
*/
public static class DaemonInnerService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(DAEMON_SERVICE_ID, new Notification());
stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
}
adb shell dumpsys activity services
查看結(jié)果看到前臺(tái)service已經(jīng)啟動(dòng)奥裸,但在通知欄里并未顯示
當(dāng)然了,我們可以結(jié)合上面這兩個(gè)方案:
創(chuàng)建一個(gè)廣播DaemonReceiver
樟氢,該廣播監(jiān)聽(tīng)某些系統(tǒng)事件廣播创倔,在廣播處理中啟動(dòng)DaemonService
public class DaemonReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
startDaemonService(context);
}
private void startDaemonService(Context context) {
if (DaemonService.isAlive()) {
return;
}
Intent serviceIntent = new Intent(context, DaemonService.class);
context.startService(serviceIntent);
}
}
鑒于當(dāng)app被殺死后是監(jiān)聽(tīng)不到系統(tǒng)廣播的,而我們還需要保持DaemonService
以確保推送TCP連接的建立霸妹,那我們可以在DaemonService
的onDestroy()
中啟動(dòng)一個(gè)新的service DaemonReStartService
, 在DaemonReStartService
中來(lái)重新啟動(dòng)DaemonService
知押。
Android中的應(yīng)用就是這么一步步被玩的卡的不要不要的,所以請(qǐng)謹(jǐn)慎使用台盯。
緩存推送消息
流程如下:
- 客戶(hù)端
- 收到推送后静盅,發(fā)送回執(zhí)消息給服務(wù)器,并存儲(chǔ)到本地?cái)?shù)據(jù)庫(kù)
- 收到的推送消息的消息ID已存儲(chǔ)到數(shù)據(jù)庫(kù)中時(shí)蒿叠,不做處理并重發(fā)回執(zhí)消息
- 服務(wù)器
- 如果客戶(hù)端未在線(xiàn),則將該條消息保存到數(shù)據(jù)庫(kù)
- 在客戶(hù)端上線(xiàn)后痊银,取出推送消息發(fā)給客戶(hù)端并標(biāo)記為已刪除
- 在取消息時(shí)注意同種類(lèi)消息是否需要合并施绎,考慮時(shí)效性只保留一定時(shí)間內(nèi)的推送消息贞绳,合并或者超時(shí)后標(biāo)記為已刪除
- 推送時(shí)在數(shù)據(jù)庫(kù)里保存記錄致稀,收到客戶(hù)端回執(zhí)后將該條消息標(biāo)記為已刪除,超時(shí)未收到回執(zhí)消息則重發(fā)消息
總結(jié)
不以用戶(hù)利益為出發(fā)點(diǎn)的手段都是耍流氓豺裆。
進(jìn)程背舨拢活必定導(dǎo)致應(yīng)用一直保持喚醒狀態(tài)一直在后臺(tái)運(yùn)行,不可避免的導(dǎo)致耗電量增加蔑歌;發(fā)送回執(zhí)消息則會(huì)額外消耗用戶(hù)流量(可以考慮一段時(shí)間內(nèi)的回執(zhí)消息合并后統(tǒng)一發(fā)送),服務(wù)器保存每條推送記錄可能會(huì)導(dǎo)致服務(wù)器壓力過(guò)大园匹。
所以劫灶,在盡可能保證用戶(hù)到達(dá)率的情況下,也要考慮節(jié)能和流量本昏,和使用設(shè)計(jì)模式一樣,凡事皆有度怔昨,萬(wàn)事不可過(guò)宿稀。