iOS Push的門道
基本功
iOS在誕生之初為了最大程度的保證用戶體驗,做了一些高瞻遠矚且影響深遠的設計。APNs(Apple Push Notification service)就是其中一項。
早期iOS設備的內存和CPU資源都很有限,為了讓前臺活躍的app擁有盡可能多的系統資源,以及節約設備電量,iOS一開始就“不允許”普通app的進程常駐后臺。這個決定很大程度上保障了用戶體驗和延長了手機的待機時間,但app的開發商需要和他們的用戶保持聯系。開發商需要有一個穩定的網絡通道能每隔一段時間推送新的內容到用戶設備。Apple決定自己來搭建維護這個通道,也就是我們今天所說的APNs。記得剛開始接觸iOS開發的時候,看到有不少開發者吐槽push機制,覺得不可控且增加了開發成本。其實稍微思考下Apple今天的平臺規模和消息量,以及所帶來的成本消耗,就能明白Apple設計這個機制所需要的智慧和魄力。一切都是為了用戶。
APNs雖然允許開發商推送消息到用戶設備,但考慮到消息的量級和成本,這個由Apple維護的長鏈接通道就不可能是無限制使用的。APNs有著諸多的限制:
可靠性。一般情況下,Apple會保證這個通道的Qaulity of Service,也就是推送的消息能及時穩定到達設備。不過一旦用戶的設備處于offline狀態,Apple只會存儲發送給用戶的最新一條push,之前發送的push會被直接丟掉。而且這最后一條離線push也是有過期時間的。一些用戶應該有過這種經歷,在使用微信的時候,明明對方發送了多條消息,卻只收到了一條push。
Payload Size。每一條push消息的包體大小是有最大限制的。Apple在文檔里清楚的說明,push只應該用來通知用戶有新的內容,而不應該用來承載內容本身。理論上payload size越小,push到達設備的概率就越高。在iOS8之前max payload size是256字節,到iOS8發布這個最大值被調整到了2048字節,再到最近的iOS9發布,引入了HTTP2.0,payload size又被設為4KB了。老版本的256字節實在有點捉襟見肘,連塞一個鏈接進去都要考慮再三。到2KB的時候就寬裕多了,已經有不少開發商開始嘗試往里面放少量的業務數據了,如果能減少打開app之后的一次網絡請求何樂而不為呢。當然4KB的想象空間會更大。Apple一直在調整這個數值,為的是給開發商更多的空間去提升用戶體驗。push慢慢變的不僅僅是一條“alert”那么簡單了。
成功率并不高。Apple雖然保證了push通道一定程度的可靠性,但push由于各種各樣的原因并不能保證較高水平的到達率。push需要向用戶申請權限,即使當時賦予了權限,后面也可能由于push過于頻繁被用戶又關掉。在夜間模式下push雖然能到達通知欄,可用戶沒有任何感知,更不用說點擊push啟動app了。還有server端token失效,這點可以通過feedback service來清理失效的token。Apple的APNs server據說每天會發送超過百億條push,在某個時間段出現峰值的時候,開發商server和Apple server連接的成功率也會降低。還有客戶端設備所處網絡環境并不穩定等等因素,使得通過push成功啟動app的成功率并不怎么高。
理解了上面這些限制,就能按照Apple的規范向用戶推送內容了。但push里面的門道遠不止這么簡單,Apple也從沒有停止過對APNs體驗的優化,類似payload size調整,interactive notification等等,每一個新的feature增加,哪怕是細微的改動,都能被聰明的開發者加以利用,以四兩撥千斤提升產品的體驗。下面就介紹一些筆者所了解到的“隱蔽門道”。
不僅僅是Local Push
很多個人開發者不具備搭建server的條件,一般會設置一個定時的local push來提醒用戶喚醒自己的app。Local push看起來似乎是個廉價的折中方案,事實上它可以更強大。APNs(一般也叫做remote push)因為有上面的各種限制,并不能很好的契合業務需要。而Local Push則不同,擁有完整的app業務上下文,還可以對push進行定制化。如果可以用Local Push替代Remote Push對體驗的提升是不言而喻的。Loca push的限制在于app必須處于運行狀態才能發起,很多聰明的開發商會開啟background task,在用戶按了home鍵之后再爭取到幾分鐘的運行時間,在這期間所有的remote push都被替換成了local push。不要小看了這幾分鐘的時間,對于很多活躍度高的app來說,按home鍵之后馬上又產生新的用戶內容的概率并不小。微信,WhatsApp都采用了這種機制來提升體驗。
叫醒你的App
開啟background task之后雖然能夠再多運行一會,但時間一到,app還是會被掛起或者kill。大部分多時候你的app是處于非活躍狀態。很多app都需要預先獲取內容,或者后臺下載文件等來減少用戶的等待時間。iOS7引入的Silent Notification和Background Fetch機制可以一定程度上滿足這種需要。silent push實現比較簡單,開啟相關后臺權限之后發送如下特定格式的json就能啟用。
喚醒app之后能處理的業務就多了,這對不少app來說是個非常實用的拓展,預加載內容也好,生成local push也好,都能提升體驗。但這種喚醒機制并不總是可靠,有時候會“叫不醒”。app如果被手動kill叫不醒,如果background fetch被用戶關閉也叫不醒,但這兩種情況在手機充電的時候又可以被叫醒。Apple有一套自己的“智能”策略。
前臺消息通道
大部分時候APNs都被用來通知用戶某個處于background的app有新內容。但其實說白了APNs不過就是一條基于長鏈接的數據通道,在app處于foreground的時候也是能收到push消息的,不過不會有任何UI展示提醒而已。處理回調的位置也是在 也就是說APNs其實還是個免費的前臺消息通道。而且有時候走APNs通道會比自己的server通道更快,如果客戶端做好數據去重,多一個輔助的數據通道當然能提升體驗。
新神通PushKit
APNs設計的初衷是避免app常駐后臺,只在用戶點收到push的時候主動去啟動app。前面提到的silent push可以在有限的場景下,無需用戶感知啟動app。但到iOS8引入PushKit framework之后,app就可以通過push隨時喚醒了,不過這個新的神通暫時還只限于voip類應用。
之前在社區看到有人提問,說微信電話本可以在用戶掛掉電話的時候,把呼叫中的push改成未接電話,好奇是怎么辦到的。因為大家都知道remote push是無法通過server動態修改push內容的,所以答案只有一個可能,app被后臺喚醒了。用戶看到的push其實是local push,而local push是可以在客戶端隨意調整的。喚醒到方式就是利用PushKit。
當然好處不僅僅是修改push內容這么簡單。WhatsApp的用戶在iOS8之后應該會有明顯的感覺,好像很少看到啟動頁面了。看起來似乎是WhatsApp開啟了voip后臺常駐運行模式,但這種模式會比較費電,一些用戶會有顧慮。真相也并非如此,WhatsApp并沒有常駐后臺,只不過是開啟了PushKit的push喚醒機制。每次用戶有新的離線消息,普通文本或者是voip call,app都會先被后臺喚醒,再從server拉取離線消息,最后生成local push。等用戶點擊local push啟動app的時候,沒有啟動頁面,沒有connecting和loading,所有的數據已經準備就緒,就好像WhatsApp一直在后臺運行一樣。也就是說,WhatsApp其實已經把所有的push都換成了local push。驗證方法也很簡單:
- 手動kill WhatsApp。
- 手機進入飛行模式。
- 收一條離線消息。
- 使用tcpdump開始監聽iphone網絡包,關閉飛行模式。
- 這時候,app被push喚醒,能看到如下圖一條WhatsApp相關的域名解析,說明app被啟動了。而且能看到很多后續的服務器交互(拉取離線消息之類)。
2 微信不知道是出于什么考慮,既沒有開啟voip后臺常駐模式,也沒有利用PushKit喚醒機制。每次收到消息之后打開app,都是先看到地球,連接中,收取中,到真正看到最新消息經常需要3s以上。PushKit已經沒有電量方面的額外損耗了,對voip類應用的體驗提升非常之大。
具體怎么實現PushKit可以參照文章末尾的鏈接地址。
總結
關于push這條長鏈接通道,Apple幾乎在每次的iOS新版本里都會增加一些feature。為了控制新feature帶來的影響,每次改動都不多,但怎么利用這些feature就看開發者各自都功力了。對用戶體驗帶來的改變遠不止官方文檔上介紹的那么簡單,只有多思考,時刻關注行業最新動態,才能發掘更多的隱藏“門道”。