天天看點

網易雲信 IM 推送保障及網絡優化實踐

本文根據網易雲信Android端進階技術架構師 周江華在 MDCC 2016 移動開發者大會上的演講整理而成,PPT 下載下傳位址:https://github.com/MDCC2016/Android-Session-Slides。
網易雲信 IM 推送保障及網絡優化實踐

網易雲信Android端進階技術架構師 周江華

大家好,我是來自網易雲信的周江華,從 2009年畢業至今,我一直在從事 IM 用戶端的研發工作,最早是 Windows 用戶端開發,直至 2011 年開始轉向移動端,先是 iOS,到 2012 年開始進行 Android 端的開發,從 2014 年到現在,一直負責雲信 Android SDK 的開發工作。今天,我想和大家分享的是雲信在保障 IM 推送和移動網絡優化方面的一些實踐經驗。

對于移動 App 來說,除了簡單的工具類 App 外,IM 功能可謂非常重要,它能夠建立起人與人之間的連接配接。社交類産品中,使用者與使用者之間的溝通可以産生出更好的使用者粘性。電商類産品,使用者與商家的連接配接能極大的促進溝通效率,降低溝通成本。教育類産品,學生與老師的連接配接讓線上教育猶如課堂。醫療類 App,醫生與患者的溝通讓醫生把脈更加準确。

IM 應用如此之廣泛,那麼,IM 是什麼,有些什麼要求呢?IM 由兩個字組成:Instant,Messaging。即時性要求有新消息時能夠立即收到,如果程式在背景,則要能立即收到推送通知。而通信則要求穩定可靠,系統不當機,程式不崩潰,安全,傳遞消息時不會被攔截監聽,消息不丢,順序不亂,不重複,如果包含音視訊聊天,則要求延遲低,流暢不卡頓。這兩點說起來容易,但是,要真正做出一套穩定可靠的商用級IM系統,挑戰非常之多。下面,我會挑選影響 IM 即時性和消息穩定性最核心的兩個問題,來看看都有哪些障礙,以及雲信是如何去實踐克服這些障礙的。

首先第一個問題是消息推送,在 iOS 端有 APNS 做推送,相當穩定。Android 本身也有 GCM 可以用,但是在國内,我們有一個叫作“牆”的東西,直接就把 GCM 等 Google 服務全部擋在外面了,導緻國内根本無法運用。對于 IM,當 App 退到背景,是必須還能夠收到新消息提醒的,沒有 GCM,怎麼辦?唯一能做的,就是背景運作了。Android 從設計上,就是支援真背景運作的,背景運作的特性也是 Android 現在能如此成功的原因之一。但另一面,Android 長久以來一直擺脫不了的卡頓、耗電等壞名聲,背景運作也拖不了幹系。是以,系統對于背景運作也不會放任自流。App 想要在背景運作,需要面對不少的障礙。

第一個障礙是 Android 的 Low Memory Killer 機制。手機的記憶體畢竟是有限的,當背景運作的程序越來越多,記憶體剩餘量也就随之減少。當有一個新的 App 想要啟動,如果此時記憶體不夠了,LMK 機制就會啟動,從正在運作的程序中挑選一個清理掉,釋放出空間,然後新的 App 就可以雲信了。這個挑選過程顯然不會随機拼人品,LMK 有兩個尺度去評判。一個是程序優先級,優先級越低,被清理的可能性越大,另一個是記憶體占用,占的記憶體越多,被清理的權重自然也越大。

因為 LMK 機制的存在,雖然 App 允許在背景運作,但同樣也面臨随時被清理的風險。是以,我們需要在被清理後及時的重新啟動。正常的,有 4 種方式能夠做到:

  1. sticky service,就是在 Service 的

    onStartCommand

    中傳回 sticky flag,這樣當 service 被 kill 掉後,系統會将它加入重新開機的 pending 清單,在後面合适的時機再把 service 重新開機;
  2. alarm,鬧鐘,有循環鬧鐘和一次性鬧鐘兩種,在鬧鐘觸發後啟動對應的元件;
  3. 在 Manifest 檔案中靜态注冊的 Receiver,通過監聽各種系統事件,比如開機、網絡變化、mount/unmounts 等。在這些事件發生時啟動元件,因為這種方式會造成在這些事件發生時系統容易卡頓,在7.0裡面,Android增加了限制;
  4. JobScheduler,這是在 5.0 裡面新增的,允許 App 在特定事件發生時做一些動作,比如充電、切換到 Wi-Fi 等。
網易雲信 IM 推送保障及網絡優化實踐

雖說無論怎麼做,App 終究免不了一死,但通過對照 LMK 的評判準則,我們還是可以降低 App 被清理的機率的。第一個就是降低程序的記憶體占用。如果采用單程序的模式,由于程序中包含了 UI、Webview、各種圖檔緩存等内容,記憶體必然會居高不下,降不下來。IM 軟體一般都會采用雙程序甚至多程序的政策,将 push 程序獨立出來,在 push 程序裡隻處理網絡連接配接和 push 業務,不參與任何其他業務邏輯,更不包含任何 UI。

網易雲信 IM 推送保障及網絡優化實踐
網易雲信 IM 推送保障及網絡優化實踐

我們來看一下雲信 Demo 的程序記憶體占用情況。上面一個是主程序,看第四列 PSS 的資料,記憶體占用是 50M 左右,下面一個是 push 程序,記憶體占用隻有 10M 左右。當處于背景時,push 程序被清理機率比 UI 主程序低很多。

網易雲信 IM 推送保障及網絡優化實踐

降低被清理機率的第二個手段是提升程序優先級。我們先看這個例子,這是綠色守護的一個截圖,我們評價其他 App 的行為,也不打廣告,是以這裡對 icon 和名字做了模糊處理。看最上面,這一組是“暫不自動休眠”,因為這裡列出的兩個 App 的狀态都是工作中,對應的程序優先級是“可視程序”。但這兩個 App 并沒有提供桌面小部門在運作,也沒有訓示前台服務的常駐通知欄提醒,事實上,它們就隻是在背景運而已。通常程序退到背景後,其程序優先級類型就變成了較低的背景程序,而不是這樣的“可視程序”,它們是通過什麼方法來提升優先級,降低被清理機率的呢?

網易雲信 IM 推送保障及網絡優化實踐

Android 在設計前台服務上有一個漏洞,通過兩個服務配合,我們就能建立一個隐形的前台服務。這裡有兩個已經啟動的 service: A 和 B。先在 A 中調用 startForeground,提供一個 NOTIFY_ID, 然後 A 就變成前台服務了,同時有了一個 ID 為 NOTIFY_ID 的常駐通知欄提醒,然後我們在 B 中也調用 startForeground,提供相同的 NOTIFY_ID, B 也變成了前台服務,因為兩個通知 ID 相同,是以這一次就不會建立新的通知欄提醒了。然後再在 A 中調用 stopForeground,A 的前台屬性被取消,同時,常駐通知欄提醒也會被移除,但是,service B 并不會受到任何影響,還是前台服務,這是再把 A 停掉,程序就隻剩下前台服務 B 了,程序也變成了前台程序,但使用者不會有任何感覺。

正常來說,做了上面三步之後,我們的程序就能夠比較穩定地在背景運作了。但是後來發現,在有些情況下,我們的推送程序卻永遠起不來。跟蹤之後發現,除了系統能夠殺掉背景運作的程序外,使用者也一樣是可以殺死程序的。使用者殺掉程序的方式有兩種,一種是在最近任務清單中将 App 劃掉,這種方式和系統殺掉程序效果相同。另外一種就是通過這裡的 force stop,這種方式比系統清理更加徹底。不但 App 正在運作的程序會被清理,App 目前在重新開機清單中的待重新開機服務,注冊的各種鬧鐘、事件監聽元件等都會被移除,除非使用者在主動點選或者系統重新開機等外力,App 沒法再自己重新爬起來了。

我們後來還發現,在有些國内的像 MIUI 一類的 ROM 上,使用者從最近任務清單中将 App 移除,效果竟然也是 force stop。正常來說,如果是使用者主動操作,我們 App 本身也不應該再重新開機了。但有些時候這個并不是使用者本意。況且,對于 IM 軟體來說,消息提送是一定要得到保障的,否則不明正确的吃瓜群衆們會覺得是我們軟體不行,連消息推送都做不好。是以,這時候還是應該想辦法繼續維持背景運作。那麼,又有哪些辦法呢?

網易雲信 IM 推送保障及網絡優化實踐

第一個是通過兩次 fork 加上 exec 的方式。兩個 fork 後,第一次 fork 的程序退出,第二次 fork 出來的程序就會被 init 程序領養。使用者此時再 force stop,因為這個程序父程序是 init,而不是 Zygote,是以不會被清理。由于這個程序還是從 Android 程序 fork 出來的,帶有 Android 運作時環境以及父程序的資源,是以記憶體會比較大。這裡可以再通過 exec 指令,打開一個純 Linux 的可執行檔案,開啟一個 daemon 程序,其記憶體占用大概隻有100K+,對使用者也就完全無感了。利用這個背景程序,可以定時地将 push 程序拉起來。此種方式隻在 5.0 以下的系統中有效,在 4.4 及以上系統中,SELinux 特性是強制開啟的,exec 沒有權限執行,同時在 5.0 之後,ActivityManager 在做 force stop 以及移除任務時,隻要是具有相同的 uid 的程序,就會全部清理掉,不再漏掉沒有虛拟機環境的程序。

最後一個背景保活的手段是一個大殺器,也是帶有強烈的中國特色。因為前面所列的所有保活手段都不是那麼保險,是以想出來這麼一個互相保活的方式。當一個 App 程序起來後,它就去掃描已安裝的應用清單,看看有沒有自己的兄弟姐妹。比如說同一個長的 App,或者是內建了同一個 SDK 的 App,如果有,就把這些 App 都拉起來。這也就是現在比較出名的“全家桶”方案。雖說這種方法确實能夠帶來較高的背景存活率,特别是那些大廠和應用廣泛的 SDK,但是這種方式對于使用者的傷害也非常大,如果有背景推送的必要性,且不會對使用者體驗造成太大傷害時,此方式還可以使用,但如果隻是為了推廣告,則會對使用者造成傷害,反過來,也可能會導緻使用者直接解除安裝 App。

現在,因為“全家桶”實在是太令人讨厭,現在各種手機管理軟體都會對這種喚醒方式做限制,特别是在 Root 過的機器上,可以做到完全切斷這些喚醒路徑。同時,很多 ROM 也會自帶管理軟體,限制背景運作和背景喚醒,以便給裝置換取更長的續航。在目前國内的 Android 生态環境中,無論采用什麼方式,想要一直在背景運作時越來越難了,我們需要重新想另外的辦法來保障消息推送。另一方面,我們作為開發者,也有義務為使用者提供更好體驗的軟體,而不是無休止的在背景浪費使用者的資源。

其實,對于 IM 來說,及時的消息推送和較低的電量消耗也并非不可兼得。在傳統上,每個 IM 用戶端都會各自維護一條與伺服器的長連接配接,自己的消息和信令都在這條長連接配接上傳遞,每個 App 也獨自去心跳,斷線重連等事情。這種模式比較簡單,不同的 App 也是完全隔離的,不會互相影響。但它的缺點也非常明顯,首先是做了很多重複的事情,造成了流量和電量的無謂消耗,第二是要保證所有的程序都能在背景運作很難。優化的方向也就非常明顯了,那就是共享連接配接,現在絕大部分推送 SDK 也是這麼做的。

從這些 App 裡選出一個目前正在運作的,或者是被殺機率最低的 App 作為總代理,隻由這個代理和伺服器建立連接配接,一個手機上的所有其他 App 都通過這個代理中轉與伺服器通信。但是,IM有一個很基本的要求在這種模式下無法得到滿足:安全。所有 App 的消息都經過代理中轉,代理到伺服器的連接配接是加密、安全的,但到了代理這裡,消息都被解開了,是以代理理論上可以看到其他所有 App 的來往消息。是以,這種共享長連接配接的方式并不适用于 IM。

網易雲信 IM 推送保障及網絡優化實踐

長連接配接+推送

雖然共享長連接配接方式不合适,但仍然給我們提供了一個優化的思路。在此基礎上,我們想到了另外一個可以脫敏共享連接配接的方式:安全長連接配接加推送連接配接模式。每個 App 在使用和真正傳遞資料時,仍然獨立使用自己的安全長連接配接。而當 App 退到背景一段時間之後,則斷開長連接配接,然後每個 App 開啟一個推送代理,并選擇其中一個和雲信的推送伺服器建立連接配接,之後當 App 有新消息時,就通過這個推送連接配接傳遞。 App 可以自己控制發出的推送消息的安全級别,可以是包含說話人和消息内容,可以隻包含說話人,或者隻是一條簡單的有新消息到達的提醒文案。推送到達後,如果是代理 App 自己的消息,直接傳遞給代理 App 即可。如果是其他 App 消息,前面說到過,直接喚醒可能會失敗,而且會導緻無謂的電量消耗,是以這裡并不直接将提醒傳遞給目标 App ,而是由帶來發出一條通知欄提醒。等使用者去點選通知欄提醒後,才會把目标 App 喚醒。

網易雲信 IM 推送保障及網絡優化實踐

系統推送

現在國内的ROM中,華為和小米的系統本來是帶有推送系統,且開放給了第三方 App 的。在這兩個系統上,使用系統的推送通道明顯會更加穩定,也更加節省資源。是以在MIUI上,從長連接配接到推送通道的切換流程仍然和前面的一樣,隻是不再使用自己的推送連接配接,而是将消息轉發到MIUI的推送伺服器,然後轉給MIUI系統的推送代理,然後傳遞給雲信的 App 。華為的推送系統流程也是一樣。不過現在華為和MIUI在推送實作上有一些差別,例如MIUI的通知欄提醒是在自己的推送代理裡完成的,而華為卻是将提醒通知交給 App 自己去完成的,另外,他們的通知欄提醒的管理接口也有很多差別。在 App 沒有被禁用的情況下,兩者都可以收到推送,而如果 App 已經被禁用了,MIUI的通知欄提醒方式還可以将推送送達,而其他的推送方式則不能送達了。

以上就是在保障消息推送方面我們所能夠做的所有事情了。如果以後有更多的系統開放自己的推送系統,我們也可以選擇逐漸接入,以提高推送到達即時性,減少資源消耗。不過相應的,我們也要承受不斷加入各種系統的推送SDK,增大釋出包體積的缺點。期望Android擁有統一推送平台的那一天早點到來吧。

相對于PC的網絡環境,我總結的手機網絡有三個特點:

  • 第一個是慢,尤其是2G,3G網絡,慢的令人發指。當我們收發圖檔視訊這類比較大的檔案時,就會看到蛋疼的菊花一圈一圈不停的轉。
  • 第二個是斷,手機跟着人不停的移動,網絡也不停的在切換,從 Wi-Fi 到移動網絡,從一個基站到另一個基站,從有信号到沒信号,都可能導緻網絡中斷。有些制式的網絡,接打電話也會導緻資料網絡斷開。另外,移動基站還有 NAT 逾時,到一個連接配接上長時間空閑後,基站就會默默的将連接配接斷開,沒有任何通知。
  • 第三個是貴,這個就不用多說,看中國移動每天淨賺一個億就知道了。

在雲信整個通信系統中,我們有三種類型的連接配接:TCP,UDP,HTTP。雖說這三個并不是同一層的協定,不過畢竟都在我們的應用的更下層,是以這麼劃分也無妨。3種類型的協定對應了不同的業務應用。TCP主要是使用者長連接配接,也就是普通IM消息和信令的傳輸,UDP用于傳輸實時音視訊資料流,而HTTP則主要用在音頻,圖檔等檔案的上傳下載下傳上。對于不同的業務,我們的優化的關注點會有一些不相同。

長連接配接是雲信所有業務的基礎,使用量也是最大的,是以優化也是從基礎開始。 在這裡我們舉兩個例子。

網易雲信 IM 推送保障及網絡優化實踐

第一個是協定的選擇。前面說,長連接配接的使用量是最大,選擇一個合适的協定至關重要。如果是剛開始接觸IM開發,一般會選擇一些開源的協定,比如XMPP,SIP等。這是XMPP協定的一個請求樣例,可以看到是一段XML格式的文本資料。這是基于SIP的SIMPLE協定的一個請求樣例,可以看到是一段類似HTTP協定的文本資料。這些協定的優勢在于開源,有成熟的解決方案可以使用,擴充性好,甚至還可以和其他系統互聯互通,協定的可讀性也非常好。但是在普遍比較臃腫,備援字段很多,在昂貴的移動網絡裡面用起來會讓人覺肉疼。雲信采用的是私有的二進制協定,這是一個請求的資料樣例,這裡是把二進制資料轉為了16進制顯示出來,每個位元組這裡顯示為兩個字元。可以看到二進制協定的特點在于完全失去了可讀性,但是,卻帶來極高的表達效率,相對于文本協定,可以節省非常多的資料流量。

網易雲信 IM 推送保障及網絡優化實踐

另一個例子是登入的優化。由于移動網絡經常斷開,是以登入常常是心跳之外互動最多的協定了。使用量越大,優化就越有意義。一般而言,登入會經過這麼幾步。

第一步是LBS。這裡的LBS不是經常說的基于位址位置的服務,在不同的廠商可能也有不同的叫法,反正作用都是擷取伺服器的IP位址。像雲信這種需要提供全球服務的系統,在世界各地都要部署伺服器,使用者登入時,肯定要選擇一台最優的伺服器接入服務。通過lbs,用戶端可以擷取離自己最近,連通性最好的伺服器連接配接機IP位址,伺服器也可以據此做負載均衡。

拿到伺服器連接配接機IP後,用戶端就去連接配接該伺服器。

連接配接成功,需要有一次握手。這個握手不是TCP的三次握手,而是為了建立安全連接配接,同伺服器協商加密算法和加密密鑰。

然後就發送登入請求,這裡會帶上使用者認證資訊,本機裝置資訊等資料。

登入成功之後,就是同步資料,包括離線消息,使用者資訊,群組資訊等。一般而言,這裡不會去做全量同步,而是采用基于時間戳的增量同步。

在移動網絡上,每一次互動都需要比較長的時間,同時,每一次網絡請求電量消耗也是很大的。是以,優化的方向就是盡量減少互動次數,而方法則是合并請求,并行操作以及省略請求。

LBS和連接配接這兩個步驟是可以并行完成的。如果前面已經擷取過LBS,這裡可以有之前的緩存位址,如果沒有,可以先連一個預設位址。

其次是握手和登入也可以并行操作。在握手包中,就可以把加密後的登入包直接帶上去了。如果是斷線重連,我們還可以簡化登入,直接帶上上一次登入的會話ID,一來減少伺服器鑒權壓力,二則可以直接帶回在斷線期間是否有未讀消息等資料,如果沒有,則能直接将同步這一步省略掉。如果有,同步也可以隻做部分同步,隻去拉去離線消息即可。等到 App 切換到前台,才去同步其他的資訊。

通過這些優化,登入時間可以降為原來的1/2到1/3,登入的流量消耗也可以節省30%左右。

網易雲信 IM 推送保障及網絡優化實踐

實時音視訊對實時性要求很高,但可以容忍一定的丢包,是以我們選擇 UDP 私有協定來作為底層的傳輸協定。如果隻是普通的IM消息,對網絡情況其實不是太敏感,最多也就是慢一點,菊花轉得久一點。但對于這種視訊電話,如果網絡差了,發生了經常性卡頓,或者是延遲很高,圖像出現花屏,音視訊不同步了,這個功能其實也就相當于廢棄了。而且,音視訊資料量本身也比較大,在弱網環境下發生問題的機率就更大了。

UDP 協定是不可靠,為了提高弱網下的實時音視訊的通話效果,需要使用相關方案來做 QoS 保障:主要包括了基于 UDP 協定的擁塞控制、前向糾錯 FEC 技術及相關的重傳技術。同時網絡層需要能夠實時的探測到網絡狀态,作為底層調整 QoS 政策的依據,同時需要回調上層,來動态調整音視訊的碼率,做到音視訊碼率自适應。通過上面的 QoS 保障,我們實際測試在 20% 的随機丢包弱網環境下,音視訊通話還能夠正常進行。

第二是音頻,我們的音頻編解碼主要以Opus為主,它具備高音質,高壓縮率,高抗丢包等特性,非常适合移動網絡。我們使用智能的jitterbuffer算法來平滑由于網絡抖動引起的聲音卡頓和延遲累計問題。配合PLC丢包補償算法,來降低音頻丢包後的爆音。同時,我們使用自研的高性能降噪算法,配合回聲消除、自動增益和舒适噪音等音頻處理算法來進一步保證音頻部分的品質。

對于視訊,我們使用時域分層的H264視訊編碼器,來降低丢包對視訊流暢性的影響,同時支援動态幀率和動态分辨率,友善上層根據業務需求進行切換。現在使用者對于視訊的清晰度要求越來越高,我們的實時通話系統目前能夠支援720p。720p下純軟體編解碼對CPU開銷過大,是以在可以開啟硬體編解碼的機器上,對于需要720p清晰度的都盡量使用硬體編解碼。

由于音視訊的網絡優化如果全部細說,恐怕再加1個小時也講不完,是以這裡我隻提了一些優化的方向供大家參考,就不一一展開了。

下面再來看看對于HTTP的優化。圖檔語音是IM的必需元素,而且本身資料比較大。在弱網環境下,快速的上傳下載下傳,更少的等待時間可以帶來更好的使用者體驗。

斷點續傳可以減少因網絡原因導緻的重複傳輸,減少傳輸時間,節省流量。

圖檔預加載技術可以根據不用網絡情況,在收到消息後,就加載不同素質的預覽圖檔,甚至直接将原圖預加載,做到使用者點開即看。

上面兩個是比較基礎的優化措施,下面兩個則比較進階一點。

網易雲信 IM 推送保障及網絡優化實踐
網易雲信 IM 推送保障及網絡優化實踐

圖檔和語音這種檔案我們并沒有通過長連接配接收發,而是通過HTTP去做上傳下載下傳。傳統上通過HTTP上傳時,檔案會分為一片一片,傳完一片,收到回包,才會穿下一個分片,一直到最終傳輸完成。可以看到,伺服器傳回ack這段時間,上傳通道其實是空閑的,如果把這段時間利用起來,可以節約不少上傳時間。Pipeline就是為此而來。通過重疊利用http請求的響應等待時間,加快傳輸速度。使用pipeline,需要修改HttpClient,同時還需要伺服器提供支援。視網絡具體情況,使用pipeline後,一次上傳可以減少20%至30%的時間。

網易雲信 IM 推送保障及網絡優化實踐

正常發送語音消息需要這幾步,先錄音,然後計算hash值,然後上傳,上傳完畢後,伺服器計算一下校驗和,通過後語音消息發送成功。在前面錄制語音時,網絡其實也是空閑的。把這段時間利用起來,則可以減少後面上傳步驟的時間。優化後,流程就變成這樣。在錄制的過程中,每錄完一段,就作為一個分片直接上傳。直到最後錄完,計算好hash,再把最後一個分片帶上hash資訊上傳。這裡除了用戶端的改動,也是需要伺服器支援。伺服器在開始接收時,很多資訊都不明确,需要開辟緩存來記錄整次上傳過程。對于比較差的網絡,邊錄邊傳的效果會更好,畢竟純語音的比特率并不高,基本都能做到錄完就傳完。

以上就是我今天分享的全部内容。提升消息推送達到率和到達速度,優化網絡利用效率,節省系統資源一直都是Android開發的核心和基礎,新技術,新方法都在不停的湧現,也歡迎大家一起讨論,進步,謝謝大家。

了解最新移動開發相關資訊和技術,請關注mobilehub公衆微信号(ID: mobilehub)。

網易雲信 IM 推送保障及網絡優化實踐