再談Android客戶端進程保活
在很多移動應用中,特別是即時通信類項目中,保活是一個永遠無法避免的一個話題。保活,按照我的理解,主要包含兩部分:
網絡連接保活:如何保證消息接收實時性。
進程保活:盡量保證應用的進程不被Android係統回收。
在很早以前,談Android的保活都會涉及到進程常駐內存,如何進行性能優化等話題,今天就這些話題,做一個簡單的總結。
Android進程
在討論這個問題之前,我們首先來看一些現象級APP的進程。
搞Android的同學都知道,每一個Android應用啟動後至少對應一個進程,有的則有多個進程,大多數主流APP都會包含多個進程,因為除了主要的進程之外,還有諸如長連接、推送等進程。
查看進程
對於任何一個進程,我們都可以通過adb shell ps|grep 的方式來查看。具體方式如下:
上圖的具體含義如下:
值 | 解釋 |
---|---|
u0_a16 | USER 進程當前用戶 |
3881 | 進程ID |
873024 | 進程的虛擬內存大小 |
37108 | 實際駐留”在內存中”的內存大小 |
進程劃分
Android係統按重要性從高到低把進程的劃為了如下幾種(嚴格來說是6種)。
1,前台進程
此種進程指用戶正在使用的程序,一般係統是不會殺死前台進程的,除非用戶強製停止應用或者係統內存不足等極端情況會殺死。
主要場景:
- 某個進程持有一個正在與用戶交互的Activity,並且該Activity正處於resume的狀態。
- 某個進程持有一個Service,並且該Service與用戶正在交互的Activity綁定。
- 某個進程持有一個Service,並且該Service調用startForeground()方法使之位於前台運行。
- 某個進程持有一個Service,並且該Service正在執行它的某個生命周期回調方法,比如onCreate()、onStart()或onDestroy()。
- 某個進程持有一個BroadcastReceiver,並且BroadcastReceiver正在執行其onReceive()方法。
2,可見進程
用戶正在使用,看得到,但是摸不著,沒有覆蓋到整個屏幕,隻有屏幕的一部分可見進程不包含任何前台組件,一般係統也是不會殺死可見進程的,除非要在資源吃緊的情況下,要保持某個或多個前台進程存活。
主要場景:
- 擁有不在前台、但仍對用戶可見的 Activity(已調用onPause())。
- 擁有綁定到可見(或前台)Activity 的 Service。
3,服務進程
在內存不足以維持所有前台進程和可見進程同時運行的情況下,服務進程會被殺死。
主要場景:
- 某個進程中運行著一個Service且該Service是通過startService()啟動的,與用戶看見的界麵沒有直接關聯。
4,後台進程
後台進程,係統可能隨時終止它們,用以回收內存。
主要場景:
- 在用戶按了"back"或者"home"後,程序本身看不到了,但是其實還在運行的程序,比如Activity調用了onPause方法。
空進程
某個進程不包含任何活躍的組件時該進程就會被置為空進程,完全沒用,殺了它隻有好處沒壞處,第一個幹它。
內存閾值
上麵主要講的是進程,那麼進程是怎麼被殺的呢?這不得不提主要的一個原因:內存。在移動設備中內存往往是有限的,打開的應用越多,後台緩存的進程也越多。在係統內存不足的情況下,係統開始依據自身的一套進程回收機製來判斷要kill掉哪些進程。在Android的內存回收機製中有一個重要的概念:Low Memory Killer。
我們可以使用cat /sys/module/lowmemorykiller/parameters/minfree來查看某個手機的內存閾值。
注意這些數字的單位是page(1 page = 4 kb)。上麵的六個數字對應的就是(MB): 72,90,108,126,144,180,這些數字也就是對應的內存閥值,內存閾值在不同的手機上不一樣,一旦低於該值,Android便開始按順序關閉進程. 因此Android開始結束優先級最低的空進程,即當可用內存小於180MB(46080*4/1024)。
讀到這裏,你或許有一個疑問,假設現在內存不足,空進程都被殺光了,現在要殺後台進程,但是手機中後台進程很多,難道要一次性全部都清理掉?當然不是的,進程是有它的優先級的,這個優先級通過進程的adj值來反映,它是linux內核分配給每個係統進程的一個值,代表進程的優先級,進程回收機製就是根據這個優先級來決定是否進行回收,adj值定義在com.android.server.am.ProcessList類中,這個類路徑是${android-sdk-path}\sources\android-23\com\android\server\am\ProcessList.java。oom_adj的值越小,進程的優先級越高,普通進程oom_adj值是大於等於0的,而係統進程oom_adj的值是小於0的,我們可以通過cat /proc/進程id/oom_adj可以看到當前進程的adj值。
看到adj值是0,0就代表這個進程是屬於前台進程,我們再按下Back鍵,將應用至於後台,再次查看。
adj值變成了8,8代表這個進程是屬於不活躍的進程。關於oom_adj進程的相關內容可以參考下表:
adj級別 | 值 | 解釋 |
---|---|---|
UNKNOWN_ADJ | 16 | 預留的最低級別,一般對於緩存的進程才有可能設置成這個級別 |
CACHED_APP_MAX_ADJ | 15 | 緩存進程,空進程,在內存不足的情況下就會優先被kill |
CACHED_APP_MIN_ADJ | 9 | 緩存進程,也就是空進程 |
SERVICE_B_ADJ | 8 | 不活躍的進程 |
PREVIOUS_APP_ADJ | 7 | 切換進程 |
HOME_APP_ADJ | 6 | 與Home交互的進程 |
SERVICE_ADJ | 5 | 有Service的進程 |
HEAVY_WEIGHT_APP_ADJ | 4 | 高權重進程 |
BACKUP_APP_ADJ | 3 | 正在備份的進程 |
PERCEPTIBLE_APP_ADJ | 2 | 可感知的進程,比如那種播放音樂 |
VISIBLE_APP_ADJ | 1 | 可見進程,如當前的Activity |
FOREGROUND_APP_ADJ | 0 | 前台進程 |
PERSISTENT_SERVICE_ADJ | -11 | 重要進程 |
PERSISTENT_PROC_ADJ | -12 | 核心進程 |
SYSTEM_ADJ | -16 | 係統進程 |
NATIVE_ADJ | -17 | 係統起的Native進程 |
說明:上表的數字可能在不同係統會有一定的出入。
下麵按照網絡保活和進程保活來給大家介紹保活的一些策略。
網絡連接保活
網絡保活,業界主要手段有:
a. GCM;
b. 公共的第三方push通道(信鴿等);
c. 自身跟服務器通過輪詢,或者長連接;
GCM即Google Cloud Messaging,主要用於消息推送的,即使在應用沒有起來的情況下,客戶端也能通過GCM收到來自服務器的消息。GCM支持Android、IOS和Chrome。由於GCM需要google service支持,在國內基本不能用,經常會斷線。
push很多也是基於長連接實現的,早年的微信,直接通過Java socket 實現。所以後麵我們直接談長連接。
長連接實現包括幾個要素:
a. 網絡切換或者初始化時 server ip 的獲取。
b. 連接前的 ip篩選,出錯後ip 的拋棄。
c. 維護長連接的心跳。
d. 服務器通過長連notify。
e. 選擇使用長連通道的業務。
f. 斷開後重連的策略。
今天,我們討論重點即時聊天中的心跳和 notify 機製。
1,心跳機製
通過定期的數據包,對抗NAT超時(一般會設置為5-10秒)。以下是部分地區網絡NAT 超時統計。
說明:
a. 連接後主動到服務器Sync拉取一次數據,確保連接過程的新消息。
b. 心跳周期的Alarm 喚醒後,一般有幾秒的cpu 時間,無需wakelock。
c. 心跳後的Alarm防止發送超時,如服務器正常回包,該Alarm 取消。
d. 如果服務器回包,係統通過網絡喚醒,無需wakelock。
流程基於兩個係統特性:
a. Alarm喚醒後,足夠cpu時間發包。
b. 網絡回包可喚醒機器。
特別是b項,假如Android封堵該特性,那就隻能用GCM了。API level >= 23的doze就關閉所有的網絡, alarm等。Google也最終在6.0版本加入REQUEST_IGNORE_BATTERY_OPTIMIZATIONS權限。
2,動態心跳
4.5min心跳周期是穩定可靠的,但無法確定是最大值。通過終端的嚐試,可以獲取到特定用戶網絡下,心跳的最大值。引入該特性的背景:
a. 運營商的信令風暴
b. 運營商網絡換代,NAT超時趨於增大
c. Alarm耗電,心跳耗流量。
動態心跳引入下列狀態:
a. 前台活躍態:亮屏,微信在前台, 周期minHeart (4.5min) ,保證體驗。
b. 後台活躍態:微信在後台10分鍾內,周期minHeart ,保證體驗。
c. 自適應計算態:步增心跳,嚐試獲取最大心跳周期(sucHeart)。
d. 後台穩定態:通過最大周期,保持穩定心跳。
下麵是自適應計算態流程:
在自適應態:
a. curHeart初始值為minHeart , 步增(heartStep)為1分鍾。
b. curHeart 失敗5次, 意味著整個自適應態最多隻有5分鍾無法接收消息。
c. 結束後,如果sucHeart > minHeart,會減去10s(避開臨界),為該網絡下的穩定周期。
d. 進入穩定態時,要求連接連續三次成功minHeart心跳周期,再使用sucHeart。
3,notify機製
網絡保活的意義在於消息實時。通過長連接,即時通信類產品有下列機製保證消息的實時。
Sync:
通過Sync CGI直接請求後台數據。Sync 通過後台和終端的seq值對比,判斷該下發哪些消息。終端正常處理消息後,seq更新為最新值。
Sync 的主要場景:
a. 長連無法建立時,通過Sync 定期輪詢;
b. 微信切到前台時,觸發Sync(保命機製);
c. 長連建立完成,立即觸發Sync,防止連接過程漏消息;
d. 接收到Notify 或者 gcm 後,終端觸發Sync 接收消息。
Notify:
類似於GCM。通過長連接,後台發出僅帶seq的小包,終端根據seq決定是否觸發Sync拉取消息。
NotifyData:
在長連穩定, Notify機製正常的情況下(保證seq的同步)。後台直接推送消息內容,節省1個RTT (Sync) 消息接收時間。終端收到內容後,帶上seq回應NotifyAck,確認成功。這裏會出現Notify和NotifyData狀態互相切換的情況:
如NotifyData 後,服務器在沒收到NotifyAck,而有新消息的情況下,會切換回到Notify,Sync可能需要冗餘之前NotifyData的消息。終端要保證串行處理NotifyData和Sync ,否則seq可能回退。
GCM:
隻要機器上有GMS ,啟動時就嚐試注冊GCM,並通知後台。服務器會根據終端是否保持長連,決定是否由GCM通知。GCM主要針對國外比較複雜的網絡環境。
進程保活
在Android係統裏,進程被殺的原因通常為以下幾個方麵:
a. 應用Crash;
b. 係統回收內存;
c. 用戶觸發;
d. 第三方root權限app。
下麵分享幾個微信和qq關於進程保活的幾個方法:
1,進程拆分
俗話說,雞蛋不能放一個籃子裏麵,那麼為了保活,我們也可以將進程拆分為幾個。
例如,上圖是微信應用的幾個進程:
a. push主要用於網絡交互,沒有UI
b. worker就是用戶看到的主要UI
c. tools主要包含gallery和webview
這樣,進程通過拆分之後,單個進程被回收了並不影響其他的進程。拆分網絡進程,確實就是為了減少進程回收帶來的網絡斷開。
可以看到push的內存要遠遠小於worker。而且push的工作性質穩定,內存增長會非常少。這樣就可以保證,盡量的減少push 被殺的可能。為了提高線程存活的概率,這裏啟動一個純C/C++ 的進程,而不是Java run time。
2,及時拉起
係統回收不可避免,及時重新拉起的手段主要依賴係統特性。從上圖看到, push有AlarmReceiver, ConnectReceiver,BootReceiver。這些receiver 都可以在push被殺後,重新拉起。特別AlarmReceiver ,結合心跳邏輯,微信被殺後,重新拉起最多一個心跳周期。
而對於worker,除了用戶UI操作啟動。在接收消息,或者網絡切換等事件, push也會通過LocalBroadcast,重新拉起worker。這種拉起的worker ,大部分初始化已經完成,也能大大提高用戶點擊微信的啟動速度。
曆史原因,我們在push和worker通信使用Broadcast和AIDL。實際上,我一直不喜歡這裏的實現,AIDL代碼冗餘多, broadcast效率低。歡迎大家分享更好的思路或者方法。
3,進程優先級
前麵說過Low Memory Killer機製,Low Memory Killer 機製決定是否殺進程除了內存大小,還有進程優先級。這個前麵也說過。從這個原理來說,我們可以通過提高進程的優先級來保活。
值得注意的是,Android 的前台service機製。但該機製的缺陷是通知欄保留了圖標。
對於 API level < 18 :調用startForeground(ID, new Notification()),發送空的Notification ,圖標則不會顯示。
對於 API level >= 18:在需要提優先級的service A啟動一個InnerService,兩個服務同時startForeground,且綁定同樣的 ID。Stop 掉InnerService ,這樣通知欄圖標即被移除。
最後更新:2017-09-10 00:02:35