對於移動APP來說,IM功能正變得越來越重要,它能夠建立起人與人之間的連線。社交類產品中,使用者與使用者之間的溝通可以產生出更好的使用者粘性。
在複雜的 Android 生態環境下,多種因素都會造成訊息推送不能及時達到客戶端。另外,不穩定的行動網路也給資料傳輸的速率和可靠性增加了障礙。
本文詳解了網易雲信IM SDK在應對弱網環境、移動端硬體限制以及Android複雜的生態現狀時的探索與心得.如何實現不影響使用者體驗的後臺保活,改善的長連線加推送組合方案,以及在弱網環境大資料傳輸的優化實踐。
帶著思考閱讀
- 什麼是IM
- IM SDK如何實現不影響使用者體驗的後臺保活
- 如何做長連線加推送組合方案
- 如何在弱網環境下優化大資料傳輸
IM的定義
IM由兩個字組成:Instant,Messaging。
即時性要求有新訊息時能夠立即收到,如果程式在後臺,則要能立即收到推送通知。
通訊則要求穩定可靠,系統不當機,程式不崩潰,安全,傳遞訊息時不會被攔截監聽,訊息不丟,順序不亂,不重複,如果包含音視訊聊天,則要求延遲低,流暢不卡頓。
要真正做出一套穩定可靠的商用級IM系統,挑戰非常大。
第一個問題是訊息推送。iOS有 APNS做推送,相當穩定。Android本身也有GCM可以用,但是在國內有“牆”,直接就把GCM等等google的服務全部擋在外面。為了實現即時穩定的訊息推送,從易信時代開始,網易就開始研究,隨著時間的推移,困難和方法也在不停的變化。
對於IM,當APP退到後臺,是必須還能夠收到新訊息提醒的,沒有GCM,怎麼辦?在最初,唯一能做的,就是後臺執行了。這幾乎是接收推送的唯一途徑,就算是到現在,也是最主要的途徑。Android從設計上,就是支援真後臺執行的,後臺執行的特性也是Android現在能如此成功的原因之一,但另一面,Android長久以來一直襬脫不了的卡頓,耗電等壞名聲,後臺執行也拖不了干係。因此,系統對於後臺執行也不會放任自流。
APP在後臺執行所面對的四大障礙
第一個障礙是Android的Low Memory Killer機制。手機的記憶體有限,當後臺執行的程式越來越多,記憶體剩餘量也就隨之減少。當有一個新的APP想要啟動,如果記憶體不夠,LMK機制就會啟動,從正在執行的程式中挑選一個清理掉,釋放出空間,然後新的APP就可以執行了。
LMK有兩個尺度去評判。一個是程式優先順序,優先順序越低,被清理的可能性越大,另一個是記憶體佔用,佔的記憶體越多,被清理的權重自然也越大。
因為LMK機制的存在,雖然APP允許在後臺執行,但同樣也面臨隨時被清理的風險。因此,網易需要在被清理後及時的重新啟動。
第二個障礙是alarm,鬧鐘,有迴圈鬧鐘和一次性鬧鐘兩種,在鬧鐘觸發後啟動對應的元件。
第三個障礙是在Manifest檔案中靜態註冊的Receiver,通過監聽各種系統事件,比如開機,網路變化,mount/unmounts等,在這些事件發生時啟動元件,因為這種方式會造成在這些事件發生時系統容易卡頓,在7.0裡面,Android增加了限制。
第四個障礙是JobScheduler,這是在5.0裡面新增的,允許APP在特定事件發生時做一些動作,比如充電,切換到wifi等。
雖說無論怎麼做,APP終究免不了一死,但通過對照LMK的評判準則,還是可以降低APP被清理的概率的。第一個就是降低程式的記憶體佔用。如果採用單程式的模式,由於程式中包含了UI,Webview,各種圖片快取等內容,記憶體必然會居高不下,降不下來。IM軟體一般都會採用雙程式甚至多程式的策略,將push程式獨立出來,在push程式裡只處理網路連線和push業務,不參與任何其他業務邏輯,更不包含任何UI。
以下是網易雲信Android SDK的架構,按照分層的結構模式設計。最底下青色的一層是push層,他就是作為一個獨立程式執行的。他只負責處理網路長連線的相關工作,比如安全加密,心跳,鑑權,封包解包等工作,所有業務邏輯都交給UI程式的服務模組去做。來看一下雲信demo的程式記憶體佔用情況。上面一個是主程式,看第四列PSS的資料,記憶體佔用是50M左右,下面一個是push程式,記憶體佔用只有10M左右。當處於後臺時,push程式被清理概率比UI主程式低很多。
降低被清理概率的第二個手段是提升程式優先順序。先看這個例子,這是綠色守護的一個截圖,最上面是“暫不自動休眠”,因為這裡列出的兩個APP的狀態都是工作中,對應的程式優先順序是“可視程式”。但這兩個APP並沒有提供桌面小部門在執行,也沒有指示前臺服務的常駐通知欄提醒,事實上,他們就只是在後臺運而已。通常程式退到後臺後,其程式優先順序型別就變成了較低的後臺程式,而不是這樣的“可視程式”,他們是通過什麼方法來提升優先順序,降低被清理概率呢?
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了,程式也變成了前臺程式,但使用者不會有任何感知。
正常來說,做了上面3步之後,程式就能夠比較穩定的在後臺執行了。
但在有些情況下,推送程式卻永遠起不來。跟蹤之後發現,除了系統能夠殺掉後臺執行的程式外,使用者也一樣是可以殺死程式的。使用者殺掉程式的方式有兩種,一種是在最近任務列表中將app劃掉,這種方式和系統殺掉程式效果相同。另外一種就是通過這裡的force stop,這種方式比系統清理更加徹底。不但app正在執行的程式會被清理,app當前在重啟列表中的待重啟服務,註冊的各種鬧鐘,事件監聽元件等都會被移除,除非使用者在主動點選或者系統重啟等外力,app沒法再自己重新爬起來了。
在有些國內的像MIUI一類的ROM上,使用者從最近任務列表中將app移除,效果竟然也是force stop。正常來說,如果是使用者主動操作,app本身也不應該再重啟了。但有些時候這個並不是使用者本意,況且,對於IM軟體來說,訊息推送是一定要得到保障的,否則不明正確的吃瓜群眾們會覺得是軟體不行,連訊息推送都做不好。
APP安卓程式保活的好辦法
第一個是通過兩次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生態環境中,無論採用什麼方式,想要一直在後臺執行時越來越難了,需要重新想另外的辦法來保障訊息推送。另一方面,作為開發者,也有義務為使用者提供更好體驗的軟體,而不是無休止的在後臺浪費使用者的資源。